From 1acbecfff56cabfe7f4366519dca630b0d2202ff Mon Sep 17 00:00:00 2001 From: Arjan Singh Date: Thu, 28 Jul 2016 00:12:08 -0700 Subject: [PATCH] Fastboot Feature Branch (#1032) * ignore store events while the session is busy (#965) * 1.1.0-beta.5 * 1.1.0 * Check for resourceName in response in Devise Authenticator * change cookie default key names to be rfc2616 compliant (#978) fixes #977 * Test for session service data being set with Ember.set (#972) * code/docs cleanup * Add tokenRefreshOffset property to OAuth2PasswordGrantAuthenticator (#840) tokenRefreshOffset determines the offset seconds before the token expiration to refresh the token. This is randomized so as to reduce race conditions between multiple tabs from refreshing at the same time. This is configurable because in some cases, the offset randomization needs to be increased to decrease the probability of the above mentioned race conditions. Once more case would be in slow internet connections, you make a call to refresh the token but the server doesn't process it in time (or receive it in time), the server will check and the token that you sent up is now expired so the refresh will fail. * cleanup transition usage in authenticated and unauthenticated route mixins (#992) no issue - fixes potential test timing issue - removes unecessary abort call * [BUGFIX] Remove Ember.Logger (#993) Ember.Logger is not substituted by noops in production. More info in emberjs/guides#1467 * [WIP] Validate server responses in authenticators (#957) * Validate response data in devise authenticator * Validate response data in OAuth2 authenticator * Add tests for oauth2 data validations * Add tests for devise data validations * Remove unncessary validations * Refactor 'restore' in devise authenticator * Fix test timeout errors * Minor cleanup * Consider resource name when validating response * Refactor devise authenticator _validate method * update dependencies (#1004) This updates Ember, Ember Data, Ember CLI etc. to the latest versions. This also fixes a lot of JSCS warnings that were introduced by the latest version of ember-suave. * Use the term "squash" when referring to collapsing commits into one (#1011) That's consistent with the term used in git-rebase and with the general public. * Add rejectWithXhr to optionally reject with XHR vs response body (#1012) Allows ember apps using ember-simple-auth to receive the whole XHR object if the backend fails, instead of the response body, if they so choose. In the case of OAuth 2.0 backends, it's been a pattern in the wild to use X- headers to send context as to why a grant has failed. Examples include API throttling, brute force lockouts, and OTP/two-factor authentication information. Selfishly, I require this change so my application can be notified when the API has locked out an account due to suspicious activity via an X- header. The decision to expose it as an option was chosen so backwards compatibility is maintained and keeps the addon simple for those who need not be concerned with complex backends. * Add fastboot-dist to npmignore (#1015) * Optionally send custom headers in authentication call (#1018) Complex systems that offer Two Factor Authentication with their OAuth 2.0 implementation need to send additional context via the HTTP headers. This pattern has been observed in the wild by such systems such as GitHub. Because of the restrictions of OAuth 2.0 RFC, only headers can be used for additional context, not request/response bodies. This could be seen as a counterpart to #1012, where using both features allow bi-directional context enabling 2FA, brute force lockouts, etc. * [fastboot-compatibility] initial work * [fastboot-compatbility] improve support * [fastboot-compatibility] Use ember-cookies@0.0.7 ember-cookies 0.0.6 ember-cookies@0.0.7 * [fastboot-compatbility] fix ember-build-cli.js * [fastboot-compatibility] fix route mixin transitions * [fastboot-compatibility] Update `session-stores/cookie` with `typeof` guard (#1) * [fastboot-compatiblity] fix tests * Use apiHost config for dummy app. better name for api host conf setting fix dummy app API endpoints * Helpful instructions for `npm run fastboot` * Restore cookie session renewal * Fix various rebase issues * ember-cli-fasboot@1.0.0-beta.7 --- .npmignore | 4 +- .travis.yml | 14 +- CHANGELOG.md | 9 + CONTRIBUTING.md | 2 +- addon/authenticators/base.js | 4 +- addon/authenticators/devise.js | 59 +++- addon/authenticators/oauth2-password-grant.js | 98 +++++- addon/authenticators/torii.js | 4 +- addon/authorizers/base.js | 4 +- addon/configuration.js | 4 +- addon/initializers/setup-session.js | 3 + addon/internal-session.js | 92 +++-- addon/mixins/application-route-mixin.js | 10 +- addon/mixins/authenticated-route-mixin.js | 11 +- addon/mixins/data-adapter-mixin.js | 6 +- addon/mixins/unauthenticated-route-mixin.js | 12 +- addon/services/session.js | 6 +- addon/session-stores/adaptive.js | 18 +- addon/session-stores/base.js | 4 +- addon/session-stores/cookie.js | 42 ++- addon/session-stores/local-storage.js | 8 +- bower.json | 10 +- ember-cli-build.js | 6 +- package.json | 61 ++-- tests/acceptance/authentication-test.js | 4 +- tests/dummy/app/adapters/application.js | 4 +- tests/dummy/app/app.js | 4 +- tests/dummy/app/authenticators/oauth2.js | 4 +- tests/dummy/app/components/login-form.js | 4 +- tests/dummy/app/components/main-navigation.js | 4 +- tests/dummy/app/controllers/application.js | 4 +- tests/dummy/app/models/account.js | 4 +- tests/dummy/app/models/post.js | 4 +- tests/dummy/app/router.js | 4 +- tests/dummy/app/routes/application.js | 4 +- tests/dummy/app/routes/auth-error.js | 4 +- tests/dummy/app/routes/login.js | 4 +- tests/dummy/app/routes/protected.js | 4 +- tests/dummy/app/services/session-account.js | 6 +- tests/helpers/destroy-app.js | 4 +- tests/helpers/module-for-acceptance.js | 16 +- tests/helpers/start-app.js | 8 +- tests/unit/authenticators/devise-test.js | 99 ++++-- .../oauth2-password-grant-test.js | 138 +++++--- tests/unit/authenticators/torii-test.js | 14 +- .../setup-session-restoration-test.js | 16 +- tests/unit/internal-session-test.js | 314 ++++++++++-------- .../mixins/application-route-mixin-test.js | 10 +- .../mixins/authenticated-route-mixin-test.js | 33 +- tests/unit/mixins/data-adapter-mixin-test.js | 6 +- .../unauthenticated-route-mixin-test.js | 33 +- tests/unit/services/session-test.js | 14 +- .../shared/cookie-store-behavior.js | 26 +- .../session-stores/shared/store-behavior.js | 6 +- vendor/ember-simple-auth/register-version.js | 2 +- 55 files changed, 765 insertions(+), 527 deletions(-) diff --git a/.npmignore b/.npmignore index a810da8c4..7e6a2739c 100644 --- a/.npmignore +++ b/.npmignore @@ -2,7 +2,9 @@ /config/ember-try.js /dist /tests +/node-tests /tmp +/fastboot-dist **/.gitkeep .bowerrc .editorconfig @@ -14,4 +16,4 @@ bower.json ember-cli-build.js testem.json -node-tests/ \ No newline at end of file +testem.js diff --git a/.travis.yml b/.travis.yml index d10754641..6fa981b71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ --- language: node_js node_js: - - "0.12" + - "4" sudo: false @@ -10,8 +10,8 @@ cache: - node_modules env: - - EMBER_TRY_SCENARIO=ember-release - EMBER_TRY_SCENARIO=ember-earliest + - EMBER_TRY_SCENARIO=ember-release - EMBER_TRY_SCENARIO=ember-beta - EMBER_TRY_SCENARIO=ember-canary @@ -21,21 +21,19 @@ matrix: - env: EMBER_TRY_SCENARIO=ember-canary before_install: - - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH - - "npm config set spin false" - - "npm install -g npm@^2" + - npm config set spin false + - npm install -g bower + - npm install phantomjs-prebuilt install: - - npm install -g bower - npm install - bower install script: - - ember try $EMBER_TRY_SCENARIO test && npm run nodetest + - ember try $EMBER_TRY_SCENARIO test --skip-cleanup notifications: email: false slack: rooms: secure: OOKD4ZksqzEBW/A3WRuOToODIxnDITqx+Esu7tdmmYPuQlMYgx4SUHv8j9OM9/ScFJiseeVGSkl45vJrHLLIITX9XSjO1RgiGZgw2heVujmGpF6CZNqvT6GsQuKIvMzmwF7IxuHdfV45Csr9Ou/Fg74TszR/4S2h4SOI4zhLg7A= - on_success: never diff --git a/CHANGELOG.md b/CHANGELOG.md index 0547aa291..5ba2dccfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 1.1.0 + +There were no changes since 1.1.0-beta.5. + +# 1.1.0-beta.5 + +* The session will now ignore session store events when it is currently + authenticating or restoring, see #965. + # 1.1.0-beta.4 * A critical bug in the cookie store causing an immediate logout after logging diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54a437e2c..bca539526 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ Here's a few steps to follow to make sure your pull request gets accepted: 3. Adhere to Ember Simple Auth's coding style; while there's no official style guide it should be clear by looking at the existing code what the agreed upon rules are. -4. Stash all your commits into one before submitting the pull request so it's +4. Squash all of your commits into one before submitting the pull request so it's easier to review them. 5. Provide a good description for your pull request - what does it add, why is that needed etc.? diff --git a/addon/authenticators/base.js b/addon/authenticators/base.js index c35a9ce10..1537b8e75 100644 --- a/addon/authenticators/base.js +++ b/addon/authenticators/base.js @@ -1,6 +1,6 @@ import Ember from 'ember'; -const { RSVP } = Ember; +const { RSVP, Evented, Object: EmberObject } = Ember; /** The base class for all authenticators. __This serves as a starting point for @@ -57,7 +57,7 @@ const { RSVP } = Ember; @uses Ember.Evented @public */ -export default Ember.Object.extend(Ember.Evented, { +export default EmberObject.extend(Evented, { /** __Triggered when the authentication data is updated by the authenticator due to an external or scheduled event__. This might happen e.g. if the diff --git a/addon/authenticators/devise.js b/addon/authenticators/devise.js index 30cd94370..be9bc9538 100644 --- a/addon/authenticators/devise.js +++ b/addon/authenticators/devise.js @@ -1,7 +1,8 @@ import Ember from 'ember'; import BaseAuthenticator from './base'; -const { RSVP: { Promise }, isEmpty, run, get, $ } = Ember; +const { RSVP: { Promise }, isEmpty, run, $: jQuery, assign: emberAssign, merge } = Ember; +const assign = emberAssign || merge; /** Authenticator that works with the Ruby gem @@ -60,6 +61,20 @@ export default BaseAuthenticator.extend({ */ identificationAttributeName: 'email', + /** + When authentication fails, the rejection callback is provided with the whole + XHR object instead of it's response JSON or text. + + This is useful for cases when the backend provides additional context not + available in the response body. + + @property rejectWithXhr + @type Boolean + @default false + @public + */ + rejectWithXhr: false, + /** Restores the session from a session data object; __returns a resolving promise when there are non-empty @@ -74,15 +89,7 @@ export default BaseAuthenticator.extend({ @public */ restore(data) { - const { tokenAttributeName, identificationAttributeName } = this.getProperties('tokenAttributeName', 'identificationAttributeName'); - const tokenAttribute = get(data, tokenAttributeName); - const identificationAttribute = get(data, identificationAttributeName); - - if (!isEmpty(tokenAttribute) && !isEmpty(identificationAttribute)) { - return Promise.resolve(data); - } else { - return Promise.reject(); - } + return this._validate(data) ? Promise.resolve(data) : Promise.reject(); }, /** @@ -105,14 +112,23 @@ export default BaseAuthenticator.extend({ */ authenticate(identification, password) { return new Promise((resolve, reject) => { - const { resourceName, identificationAttributeName } = this.getProperties('resourceName', 'identificationAttributeName'); + const useXhr = this.get('rejectWithXhr'); + const { resourceName, identificationAttributeName, tokenAttributeName } = this.getProperties('resourceName', 'identificationAttributeName', 'tokenAttributeName'); const data = {}; data[resourceName] = { password }; data[resourceName][identificationAttributeName] = identification; return this.makeRequest(data).then( - (response) => run(null, resolve, response), - (xhr) => run(null, reject, xhr.responseJSON || xhr.responseText) + (response) => { + if (this._validate(response)) { + const resourceName = this.get('resourceName'); + const _response = response[resourceName] ? response[resourceName] : response; + run(null, resolve, _response); + } else { + run(null, reject, `Check that server response includes ${tokenAttributeName} and ${identificationAttributeName}`); + } + }, + (xhr) => run(null, reject, useXhr ? xhr : (xhr.responseJSON || xhr.responseText)) ); }); }, @@ -139,7 +155,8 @@ export default BaseAuthenticator.extend({ */ makeRequest(data, options) { const serverTokenEndpoint = this.get('serverTokenEndpoint'); - const requestOptions = $.extend({}, { + let requestOptions = {}; + assign(requestOptions, { url: serverTokenEndpoint, type: 'POST', dataType: 'json', @@ -147,8 +164,18 @@ export default BaseAuthenticator.extend({ beforeSend(xhr, settings) { xhr.setRequestHeader('Accept', settings.accepts.json); } - }, options || {}); + }); + assign(requestOptions, options || {}); + + return jQuery.ajax(requestOptions); + }, + + _validate(data) { + const tokenAttributeName = this.get('tokenAttributeName'); + const identificationAttributeName = this.get('identificationAttributeName'); + const resourceName = this.get('resourceName'); + const _data = data[resourceName] ? data[resourceName] : data; - return $.ajax(requestOptions); + return !isEmpty(_data[tokenAttributeName]) && !isEmpty(_data[identificationAttributeName]); } }); diff --git a/addon/authenticators/oauth2-password-grant.js b/addon/authenticators/oauth2-password-grant.js index c91e433e3..322a8c19b 100644 --- a/addon/authenticators/oauth2-password-grant.js +++ b/addon/authenticators/oauth2-password-grant.js @@ -2,8 +2,21 @@ import Ember from 'ember'; import BaseAuthenticator from './base'; -const { RSVP, isEmpty, run, computed } = Ember; -const assign = Ember.assign || Ember.merge; +const { + RSVP, + isEmpty, + run, + computed, + makeArray, + assign: emberAssign, + merge, + A, + $: jQuery, + testing, + warn, + keys +} = Ember; +const assign = emberAssign || merge; /** Authenticator that conforms to OAuth 2 @@ -80,6 +93,28 @@ export default BaseAuthenticator.extend({ */ refreshAccessTokens: true, + /** + The offset time in milliseconds to refresh the access token. This must + return a random number. This randomization is needed because in case of + multiple tabs, we need to prevent the tabs from sending refresh token + request at the same exact moment. + + __When overriding this property, make sure to mark the overridden property + as volatile so it will actually have a different value each time it is + accessed.__ + + @property refreshAccessTokens + @type Integer + @default a random number between 5 and 10 + @public + */ + tokenRefreshOffset: computed(function() { + const min = 5; + const max = 10; + + return (Math.floor(Math.random() * min) + (max - min)) * 1000; + }).volatile(), + _refreshTokenTimeout: null, _clientIdHeader: computed('clientId', function() { @@ -91,6 +126,20 @@ export default BaseAuthenticator.extend({ } }), + /** + When authentication fails, the rejection callback is provided with the whole + XHR object instead of it's response JSON or text. + + This is useful for cases when the backend provides additional context not + available in the response body. + + @property rejectWithXhr + @type Boolean + @default false + @public + */ + rejectWithXhr: false, + /** Restores the session from a session data object; __will return a resolving promise when there is a non-empty `access_token` in the session data__ and @@ -120,7 +169,7 @@ export default BaseAuthenticator.extend({ reject(); } } else { - if (isEmpty(data['access_token'])) { + if (!this._validate(data)) { reject(); } else { this._scheduleAccessTokenRefresh(data['expires_in'], data['expires_at'], data['refresh_token']); @@ -151,28 +200,35 @@ export default BaseAuthenticator.extend({ @param {String} identification The resource owner username @param {String} password The resource owner password @param {String|Array} scope The scope of the access request (see [RFC 6749, section 3.3](http://tools.ietf.org/html/rfc6749#section-3.3)) + @param {Object} headers Optional headers that particular backends may require (for example sending 2FA challenge responses) @return {Ember.RSVP.Promise} A promise that when it resolves results in the session becoming authenticated @public */ - authenticate(identification, password, scope = []) { + authenticate(identification, password, scope = [], headers = {}) { return new RSVP.Promise((resolve, reject) => { const data = { 'grant_type': 'password', username: identification, password }; const serverTokenEndpoint = this.get('serverTokenEndpoint'); - const scopesString = Ember.makeArray(scope).join(' '); - if (!Ember.isEmpty(scopesString)) { + const useXhr = this.get('rejectWithXhr'); + const scopesString = makeArray(scope).join(' '); + if (!isEmpty(scopesString)) { data.scope = scopesString; } - this.makeRequest(serverTokenEndpoint, data).then((response) => { + this.makeRequest(serverTokenEndpoint, data, headers).then((response) => { run(() => { + if (!this._validate(response)) { + reject('access_token is missing in server response'); + } + const expiresAt = this._absolutizeExpirationTime(response['expires_in']); this._scheduleAccessTokenRefresh(response['expires_in'], expiresAt, response['refresh_token']); if (!isEmpty(expiresAt)) { response = assign(response, { 'expires_at': expiresAt }); } + resolve(response); }); }, (xhr) => { - run(null, reject, xhr.responseJSON || xhr.responseText); + run(null, reject, useXhr ? xhr : (xhr.responseJSON || xhr.responseText)); }); }); }, @@ -203,7 +259,7 @@ export default BaseAuthenticator.extend({ success.apply(this, [resolve]); } else { const requests = []; - Ember.A(['access_token', 'refresh_token']).forEach((tokenType) => { + A(['access_token', 'refresh_token']).forEach((tokenType) => { const token = data[tokenType]; if (!isEmpty(token)) { requests.push(this.makeRequest(serverTokenRevocationEndpoint, { @@ -225,24 +281,30 @@ export default BaseAuthenticator.extend({ @method makeRequest @param {String} url The request URL @param {Object} data The request data + @param {Object} headers Additional headers to send in request @return {jQuery.Deferred} A promise like jQuery.Deferred as returned by `$.ajax` @protected */ - makeRequest(url, data) { + makeRequest(url, data, headers = {}) { const options = { url, data, type: 'POST', dataType: 'json', - contentType: 'application/x-www-form-urlencoded' + contentType: 'application/x-www-form-urlencoded', + headers }; const clientIdHeader = this.get('_clientIdHeader'); if (!isEmpty(clientIdHeader)) { - options.headers = clientIdHeader; + merge(options.headers, clientIdHeader); + } + + if (isEmpty(keys(options.headers))) { + delete options.headers; } - return Ember.$.ajax(options); + return jQuery.ajax(options); }, _scheduleAccessTokenRefresh(expiresIn, expiresAt, refreshToken) { @@ -252,11 +314,11 @@ export default BaseAuthenticator.extend({ if (isEmpty(expiresAt) && !isEmpty(expiresIn)) { expiresAt = new Date(now + expiresIn * 1000).getTime(); } - const offset = (Math.floor(Math.random() * 5) + 5) * 1000; + const offset = this.get('tokenRefreshOffset'); if (!isEmpty(refreshToken) && !isEmpty(expiresAt) && expiresAt > now - offset) { run.cancel(this._refreshTokenTimeout); delete this._refreshTokenTimeout; - if (!Ember.testing) { + if (!testing) { this._refreshTokenTimeout = run.later(this, this._refreshAccessToken, expiresIn, refreshToken, expiresAt - now - offset); } } @@ -278,7 +340,7 @@ export default BaseAuthenticator.extend({ resolve(data); }); }, (xhr, status, error) => { - Ember.Logger.warn(`Access token could not be refreshed - server responded with ${error}.`); + warn(`Access token could not be refreshed - server responded with ${error}.`); reject(); }); }); @@ -288,5 +350,9 @@ export default BaseAuthenticator.extend({ if (!isEmpty(expiresIn)) { return new Date((new Date().getTime()) + expiresIn * 1000).getTime(); } + }, + + _validate(data) { + return !isEmpty(data['access_token']); } }); diff --git a/addon/authenticators/torii.js b/addon/authenticators/torii.js index 47402065d..c8f99ea9d 100644 --- a/addon/authenticators/torii.js +++ b/addon/authenticators/torii.js @@ -1,7 +1,7 @@ import Ember from 'ember'; import BaseAuthenticator from './base'; -const { RSVP, isEmpty } = Ember; +const { RSVP, isEmpty, assert, isPresent } = Ember; /** Authenticator that wraps the @@ -112,6 +112,6 @@ export default BaseAuthenticator.extend({ _assertToriiIsPresent() { const torii = this.get('torii'); - Ember.assert('You are trying to use the torii authenticator but torii is not available. Inject torii into the authenticator with "torii: Ember.inject.service()".', Ember.isPresent(torii)); + assert('You are trying to use the torii authenticator but torii is not available. Inject torii into the authenticator with "torii: Ember.inject.service()".', isPresent(torii)); } }); diff --git a/addon/authorizers/base.js b/addon/authorizers/base.js index 926539f7c..d801997ab 100644 --- a/addon/authorizers/base.js +++ b/addon/authorizers/base.js @@ -1,5 +1,7 @@ import Ember from 'ember'; +const { Object: EmberObject } = Ember; + /** The base class for all authorizers. __This serves as a starting point for implementing custom authorizers and must not be used directly.__ @@ -20,7 +22,7 @@ import Ember from 'ember'; @extends Ember.Object @public */ -export default Ember.Object.extend({ +export default EmberObject.extend({ /** Authorizes a block of code. This method will be invoked by the session service's {{#crossLink "SessionService/authorize:method"}}{{/crossLink}} diff --git a/addon/configuration.js b/addon/configuration.js index d2cbccaa2..ab1100263 100644 --- a/addon/configuration.js +++ b/addon/configuration.js @@ -1,6 +1,6 @@ import Ember from 'ember'; -const { getWithDefault } = Ember; +const { getWithDefault, typeOf } = Ember; const DEFAULTS = { baseURL: '', @@ -83,7 +83,7 @@ export default { load(config) { for (let property in this) { - if (this.hasOwnProperty(property) && Ember.typeOf(this[property]) !== 'function') { + if (this.hasOwnProperty(property) && typeOf(this[property]) !== 'function') { this[property] = getWithDefault(config, property, DEFAULTS[property]); } } diff --git a/addon/initializers/setup-session.js b/addon/initializers/setup-session.js index 05f7354c3..889c35bec 100644 --- a/addon/initializers/setup-session.js +++ b/addon/initializers/setup-session.js @@ -7,10 +7,13 @@ export default function setupSession(registry) { registry.register('session:main', InternalSession); let store = 'session-store:application'; + /* jscs:disable disallowDirectPropertyAccess */ if (Ember.testing) { + /* jscs:enable disallowDirectPropertyAccess */ store = 'session-store:test'; registry.register(store, Ephemeral); } + inject(registry, store, 'cookies', 'service:cookies'); inject(registry, 'session:main', 'store', store); } diff --git a/addon/internal-session.js b/addon/internal-session.js index f29dfaeb4..648a96ee3 100644 --- a/addon/internal-session.js +++ b/addon/internal-session.js @@ -1,10 +1,22 @@ import Ember from 'ember'; import getOwner from 'ember-getowner-polyfill'; -const { RSVP, isNone, isEmpty } = Ember; -const assign = Ember.assign || Ember.merge; - -export default Ember.ObjectProxy.extend(Ember.Evented, { +const { + RSVP, + isNone, + isEmpty, + ObjectProxy, + Evented, + assign: emberAssign, + merge, + assert, + deprecate, + set, + debug +} = Ember; +const assign = emberAssign || merge; + +export default ObjectProxy.extend(Evented, { authenticator: null, store: null, isAuthenticated: false, @@ -13,37 +25,45 @@ export default Ember.ObjectProxy.extend(Ember.Evented, { init() { this._super(...arguments); this.set('content', { authenticated: {} }); + this._busy = false; this._bindToStoreEvents(); }, authenticate(authenticatorFactory, ...args) { - Ember.assert(`Session#authenticate requires the authenticator to be specified, was "${authenticatorFactory}"!`, !isEmpty(authenticatorFactory)); + this._busy = true; + assert(`Session#authenticate requires the authenticator to be specified, was "${authenticatorFactory}"!`, !isEmpty(authenticatorFactory)); const authenticator = this._lookupAuthenticator(authenticatorFactory); - Ember.assert(`No authenticator for factory "${authenticatorFactory}" could be found!`, !isNone(authenticator)); + assert(`No authenticator for factory "${authenticatorFactory}" could be found!`, !isNone(authenticator)); return authenticator.authenticate(...args).then((content) => { + this._busy = false; return this._setup(authenticatorFactory, content, true); }, (error) => { const rejectWithError = () => RSVP.Promise.reject(error); + this._busy = false; return this._clear().then(rejectWithError, rejectWithError); }); }, invalidate() { - Ember.assert('Session#invalidate requires the session to be authenticated!', this.get('isAuthenticated')); + this._busy = true; + assert('Session#invalidate requires the session to be authenticated!', this.get('isAuthenticated')); let authenticator = this._lookupAuthenticator(this.authenticator); return authenticator.invalidate(this.content.authenticated).then(() => { authenticator.off('sessionDataUpdated'); + this._busy = false; return this._clear(true); }, (error) => { this.trigger('sessionInvalidationFailed', error); + this._busy = false; return RSVP.Promise.reject(error); }); }, restore() { + this._busy = true; const reject = () => RSVP.Promise.reject(); return this._callStoreAsync('restore').then((restoredContent) => { @@ -53,19 +73,23 @@ export default Ember.ObjectProxy.extend(Ember.Evented, { const authenticator = this._lookupAuthenticator(authenticatorFactory); return authenticator.restore(restoredContent.authenticated).then((content) => { this.set('content', restoredContent); + this._busy = false; return this._setup(authenticatorFactory, content); }, (err) => { - Ember.Logger.debug(`The authenticator "${authenticatorFactory}" rejected to restore the session - invalidating…`); + debug(`The authenticator "${authenticatorFactory}" rejected to restore the session - invalidating…`); if (err) { - Ember.Logger.debug(err); + debug(err); } + this._busy = false; return this._clearWithContent(restoredContent).then(reject, reject); }); } else { delete (restoredContent || {}).authenticated; + this._busy = false; return this._clearWithContent(restoredContent).then(reject, reject); } }, () => { + this._busy = false; return this._clear().then(reject, reject); }); }, @@ -74,7 +98,7 @@ export default Ember.ObjectProxy.extend(Ember.Evented, { const result = this.store[method](...params); if (typeof result === 'undefined' || typeof result.then === 'undefined') { - Ember.deprecate(`Ember Simple Auth: Synchronous stores have been deprecated. Make sure your custom store's ${method} method returns a promise.`, false, { + deprecate(`Ember Simple Auth: Synchronous stores have been deprecated. Make sure your custom store's ${method} method returns a promise.`, false, { id: `ember-simple-auth.session-store.synchronous-${method}`, until: '2.0.0' }); @@ -91,7 +115,7 @@ export default Ember.ObjectProxy.extend(Ember.Evented, { isAuthenticated: true, authenticator }); - Ember.set(this.content, 'authenticated', authenticatedContent); + set(this.content, 'authenticated', authenticatedContent); this._bindToAuthenticatorEvents(); return this._updateStore().then(() => { @@ -104,7 +128,7 @@ export default Ember.ObjectProxy.extend(Ember.Evented, { isAuthenticated: false, authenticator: null }); - Ember.set(this.content, 'authenticated', {}); + set(this.content, 'authenticated', {}); this.endPropertyChanges(); }); }, @@ -116,7 +140,7 @@ export default Ember.ObjectProxy.extend(Ember.Evented, { isAuthenticated: false, authenticator: null }); - Ember.set(this.content, 'authenticated', {}); + set(this.content, 'authenticated', {}); return this._updateStore().then(() => { this.endPropertyChanges(); @@ -132,7 +156,7 @@ export default Ember.ObjectProxy.extend(Ember.Evented, { }, setUnknownProperty(key, value) { - Ember.assert('"authenticated" is a reserved key used by Ember Simple Auth!', key !== 'authenticated'); + assert('"authenticated" is a reserved key used by Ember Simple Auth!', key !== 'authenticated'); let result = this._super(key, value); this._updateStore(); return result; @@ -140,8 +164,8 @@ export default Ember.ObjectProxy.extend(Ember.Evented, { _updateStore() { let data = this.content; - if (!Ember.isEmpty(this.authenticator)) { - Ember.set(data, 'authenticated', assign({ authenticator: this.authenticator }, data.authenticated || {})); + if (!isEmpty(this.authenticator)) { + set(data, 'authenticated', assign({ authenticator: this.authenticator }, data.authenticated || {})); } return this._callStoreAsync('persist', data); }, @@ -160,22 +184,28 @@ export default Ember.ObjectProxy.extend(Ember.Evented, { _bindToStoreEvents() { this.store.on('sessionDataUpdated', (content) => { - let { authenticator: authenticatorFactory } = (content.authenticated || {}); - if (!!authenticatorFactory) { - delete content.authenticated.authenticator; - const authenticator = this._lookupAuthenticator(authenticatorFactory); - authenticator.restore(content.authenticated).then((authenticatedContent) => { - this.set('content', content); - this._setup(authenticatorFactory, authenticatedContent, true); - }, (err) => { - Ember.Logger.debug(`The authenticator "${authenticatorFactory}" rejected to restore the session - invalidating…`); - if (err) { - Ember.Logger.debug(err); - } + if (!this._busy) { + this._busy = true; + let { authenticator: authenticatorFactory } = (content.authenticated || {}); + if (!!authenticatorFactory) { + delete content.authenticated.authenticator; + const authenticator = this._lookupAuthenticator(authenticatorFactory); + authenticator.restore(content.authenticated).then((authenticatedContent) => { + this.set('content', content); + this._busy = false; + this._setup(authenticatorFactory, authenticatedContent, true); + }, (err) => { + debug(`The authenticator "${authenticatorFactory}" rejected to restore the session - invalidating…`); + if (err) { + debug(err); + } + this._busy = false; + this._clearWithContent(content, true); + }); + } else { + this._busy = false; this._clearWithContent(content, true); - }); - } else { - this._clearWithContent(content, true); + } } }); }, diff --git a/addon/mixins/application-route-mixin.js b/addon/mixins/application-route-mixin.js index e773e6e93..dffc63b61 100644 --- a/addon/mixins/application-route-mixin.js +++ b/addon/mixins/application-route-mixin.js @@ -1,7 +1,7 @@ import Ember from 'ember'; import Configuration from './../configuration'; -const { inject } = Ember; +const { inject, Mixin, A, run: { bind }, testing } = Ember; /** The mixin for the application route; __defines methods that are called when @@ -41,7 +41,7 @@ const { inject } = Ember; @extends Ember.Mixin @public */ -export default Ember.Mixin.create({ +export default Mixin.create({ /** The session service. @@ -58,11 +58,11 @@ export default Ember.Mixin.create({ }, _subscribeToSessionEvents() { - Ember.A([ + A([ ['authenticationSucceeded', 'sessionAuthenticated'], ['invalidationSucceeded', 'sessionInvalidated'] ]).forEach(([event, method]) => { - this.get('session').on(event, Ember.run.bind(this, () => { + this.get('session').on(event, bind(this, () => { this[method](...arguments); })); }); @@ -107,7 +107,7 @@ export default Ember.Mixin.create({ @public */ sessionInvalidated() { - if (!Ember.testing) { + if (!testing) { window.location.replace(Configuration.baseURL); } } diff --git a/addon/mixins/authenticated-route-mixin.js b/addon/mixins/authenticated-route-mixin.js index f2e0c97a1..f570cb9ea 100644 --- a/addon/mixins/authenticated-route-mixin.js +++ b/addon/mixins/authenticated-route-mixin.js @@ -1,8 +1,7 @@ import Ember from 'ember'; -import getOwner from 'ember-getowner-polyfill'; import Configuration from './../configuration'; -const { inject: { service }, computed } = Ember; +const { inject: { service }, Mixin, assert, computed } = Ember; /** __This mixin is used to make routes accessible only if the session is @@ -23,7 +22,7 @@ const { inject: { service }, computed } = Ember; @extends Ember.Mixin @public */ -export default Ember.Mixin.create({ +export default Mixin.create({ /** The session service. @@ -61,13 +60,15 @@ export default Ember.Mixin.create({ */ beforeModel(transition) { if (!this.get('session.isAuthenticated')) { - Ember.assert('The route configured as Configuration.authenticationRoute cannot implement the AuthenticatedRouteMixin mixin as that leads to an infinite transitioning loop!', this.get('routeName') !== Configuration.authenticationRoute); + assert('The route configured as Configuration.authenticationRoute cannot implement the AuthenticatedRouteMixin mixin as that leads to an infinite transitioning loop!', this.get('routeName') !== Configuration.authenticationRoute); if (!this.get('_isFastBoot')) { transition.abort(); this.set('session.attemptedTransition', transition); } - this.transitionTo(Configuration.authenticationRoute); + + this.set('session.attemptedTransition', transition); + return this.transitionTo(Configuration.authenticationRoute); } else { return this._super(...arguments); } diff --git a/addon/mixins/data-adapter-mixin.js b/addon/mixins/data-adapter-mixin.js index b232b6afb..aea873fc3 100644 --- a/addon/mixins/data-adapter-mixin.js +++ b/addon/mixins/data-adapter-mixin.js @@ -1,6 +1,6 @@ import Ember from 'ember'; -const { service } = Ember.inject; +const { inject: { service }, Mixin, assert, isPresent } = Ember; /** __This mixin can be used to make Ember Data adapters authorize all outgoing @@ -30,7 +30,7 @@ const { service } = Ember.inject; @public */ -export default Ember.Mixin.create({ +export default Mixin.create({ /** The session service. @@ -69,7 +69,7 @@ export default Ember.Mixin.create({ */ ajaxOptions() { const authorizer = this.get('authorizer'); - Ember.assert("You're using the DataAdapterMixin without specifying an authorizer. Please add `authorizer: 'authorizer:application'` to your adapter.", Ember.isPresent(authorizer)); + assert("You're using the DataAdapterMixin without specifying an authorizer. Please add `authorizer: 'authorizer:application'` to your adapter.", isPresent(authorizer)); let hash = this._super(...arguments); let { beforeSend } = hash; diff --git a/addon/mixins/unauthenticated-route-mixin.js b/addon/mixins/unauthenticated-route-mixin.js index 5d706a65c..e921f4238 100644 --- a/addon/mixins/unauthenticated-route-mixin.js +++ b/addon/mixins/unauthenticated-route-mixin.js @@ -1,8 +1,7 @@ import Ember from 'ember'; -import getOwner from 'ember-getowner-polyfill'; import Configuration from './../configuration'; -const { inject: { service }, computed } = Ember; +const { inject: { service }, Mixin, assert, computed } = Ember; /** __This mixin is used to make routes accessible only if the session is @@ -24,7 +23,7 @@ const { inject: { service }, computed } = Ember; @extends Ember.Mixin @public */ -export default Ember.Mixin.create({ +export default Mixin.create({ /** The session service. @@ -54,15 +53,14 @@ export default Ember.Mixin.create({ @param {Transition} transition The transition that lead to this route @public */ - beforeModel(transition) { + beforeModel() { if (this.get('session').get('isAuthenticated')) { - if (!this.get('_isFastBoot')) { transition.abort(); } - Ember.assert('The route configured as Configuration.routeIfAlreadyAuthenticated cannot implement the UnauthenticatedRouteMixin mixin as that leads to an infinite transitioning loop!', this.get('routeName') !== Configuration.routeIfAlreadyAuthenticated); - this.transitionTo(Configuration.routeIfAlreadyAuthenticated); + assert('The route configured as Configuration.routeIfAlreadyAuthenticated cannot implement the UnauthenticatedRouteMixin mixin as that leads to an infinite transitioning loop!', this.get('routeName') !== Configuration.routeIfAlreadyAuthenticated); + return this.transitionTo(Configuration.routeIfAlreadyAuthenticated); } else { return this._super(...arguments); } diff --git a/addon/services/session.js b/addon/services/session.js index f1879c419..18451663c 100644 --- a/addon/services/session.js +++ b/addon/services/session.js @@ -3,7 +3,7 @@ import getOwner from 'ember-getowner-polyfill'; const SESSION_DATA_KEY_PREFIX = /^data\./; -const { computed } = Ember; +const { computed, A, Service, Evented } = Ember; /** __The session service provides access to the current session as well as @@ -25,7 +25,7 @@ const { computed } = Ember; @uses Ember.Evented @public */ -export default Ember.Service.extend(Ember.Evented, { +export default Service.extend(Evented, { /** Triggered whenever the session is successfully authenticated. This happens when the session gets authenticated via @@ -130,7 +130,7 @@ export default Ember.Service.extend(Ember.Evented, { }, _forwardSessionEvents() { - Ember.A([ + A([ 'authenticationSucceeded', 'invalidationSucceeded' ]).forEach((event) => { diff --git a/addon/session-stores/adaptive.js b/addon/session-stores/adaptive.js index 73585b4cd..598e384cb 100644 --- a/addon/session-stores/adaptive.js +++ b/addon/session-stores/adaptive.js @@ -28,10 +28,20 @@ export default Base.extend({ @property localStorageKey @type String - @default 'ember_simple_auth:session' + @default 'ember_simple_auth-session' @public */ - localStorageKey: 'ember_simple_auth:session', + localStorageKey: 'ember_simple_auth-session', + + /** + The cookie store injected by initializers/setup-session. + + This is necessary for the adaptive session store because it and only it + creates an instance of another store, bypassing the Ember registry. + Because the created store does not come from the registry, it has no + container and you cannot call getOwner within it. + */ + cookies: null, /** The domain to use for the cookie if `localStorage` is not available, e.g., @@ -51,10 +61,10 @@ export default Base.extend({ @property cookieName @type String - @default ember_simple_auth:session + @default ember_simple_auth-session @public */ - cookieName: 'ember_simple_auth:session', + cookieName: 'ember_simple_auth-session', /** The expiration time for the cookie in seconds if `localStorage` is not diff --git a/addon/session-stores/base.js b/addon/session-stores/base.js index 7832f5c43..45696176e 100644 --- a/addon/session-stores/base.js +++ b/addon/session-stores/base.js @@ -1,6 +1,6 @@ import Ember from 'ember'; -const { RSVP } = Ember; +const { RSVP, Object: EmberObject, Evented } = Ember; /** The base class for all session stores. __This serves as a starting point for @@ -15,7 +15,7 @@ const { RSVP } = Ember; @uses Ember.Evented @public */ -export default Ember.Object.extend(Ember.Evented, { +export default EmberObject.extend(Evented, { /** Triggered when the session store's data changes due to an external event, e.g. from another tab or window of the same application. The session diff --git a/addon/session-stores/cookie.js b/addon/session-stores/cookie.js index 7116e19cf..93399f17a 100644 --- a/addon/session-stores/cookie.js +++ b/addon/session-stores/cookie.js @@ -3,7 +3,7 @@ import BaseStore from './base'; import objectsAreEqual from '../utils/objects-are-equal'; import getOwner from 'ember-getowner-polyfill'; -const { RSVP, computed, run: { next } } = Ember; +const { RSVP, computed, inject: { service }, run: { next, cancel, later }, isEmpty, typeOf, testing } = Ember; /** Session store that persists data in a cookie. @@ -60,10 +60,10 @@ export default BaseStore.extend({ @property cookieName @type String - @default ember_simple_auth:session + @default ember_simple_auth-session @public */ - cookieName: 'ember_simple_auth:session', + cookieName: 'ember_simple_auth-session', /** The expiration time for the cookie in seconds. A value of `null` will make @@ -77,23 +77,19 @@ export default BaseStore.extend({ */ cookieExpirationTime: null, - _cookies: computed(function() { - let owner = getOwner(this); - - return owner.lookup('service:cookies'); - }), + _cookies: service('cookies'), _fastboot: computed(function() { let owner = getOwner(this); - return owner.lookup('service:fastboot'); + return owner && owner.lookup('service:fastboot'); }), _secureCookies: computed(function() { if (this.get('_fastboot.isFastBoot')) { - return this.get('_fastboot._fastbootInfo.request.hostname').indexOf('https:') === 0; + return this.get('_fastboot.request.host').indexOf('https:') === 0; } else { - return window.location.protocol === 'https:' + return window.location.protocol === 'https:'; } }).volatile(), @@ -105,14 +101,14 @@ export default BaseStore.extend({ if (this.get('_fastboot.isFastBoot')) { return false; } else { - const visibilityState = document.visibilityState || 'visible'; + const visibilityState = typeof document !== 'undefined' ? document.visibilityState || 'visible' : false; return visibilityState === 'visible'; } }).volatile(), init() { this._super(...arguments); -/* + if (!this.get('_fastboot.isFastBoot')) { next(() => { this._syncData().then(() => { @@ -121,7 +117,7 @@ export default BaseStore.extend({ }); } else { this._renew(); - }*/ + } }, /** @@ -149,7 +145,7 @@ export default BaseStore.extend({ */ restore() { let data = this._read(this.cookieName); - if (Ember.isEmpty(data)) { + if (isEmpty(data)) { return RSVP.resolve({}); } else { return RSVP.resolve(JSON.parse(data)); @@ -194,17 +190,17 @@ export default BaseStore.extend({ this._lastData = data; this.trigger('sessionDataUpdated', data); } - if (!Ember.testing) { - Ember.run.cancel(this._syncDataTimeout); - this._syncDataTimeout = Ember.run.later(this, this._syncData, 500); + if (!testing) { + cancel(this._syncDataTimeout); + this._syncDataTimeout = later(this, this._syncData, 500); } }); }, _renew() { return this.restore().then((data) => { - if (!Ember.isEmpty(data) && data !== {}) { - data = Ember.typeOf(data) === 'string' ? data : JSON.stringify(data || {}); + if (!isEmpty(data) && data !== {}) { + data = typeOf(data) === 'string' ? data : JSON.stringify(data || {}); let expiration = this._calculateExpirationTime(); this._write(data, expiration); } @@ -212,9 +208,9 @@ export default BaseStore.extend({ }, _renewExpiration() { - if (!Ember.testing) { - Ember.run.cancel(this._renewExpirationTimeout); - this._renewExpirationTimeout = Ember.run.later(this, this._renewExpiration, 60000); + if (!testing) { + cancel(this._renewExpirationTimeout); + this._renewExpirationTimeout = later(this, this._renewExpiration, 60000); } if (this.get('_isPageVisible')) { return this._renew(); diff --git a/addon/session-stores/local-storage.js b/addon/session-stores/local-storage.js index 68c2e6395..fc555420f 100644 --- a/addon/session-stores/local-storage.js +++ b/addon/session-stores/local-storage.js @@ -3,7 +3,7 @@ import Ember from 'ember'; import BaseStore from './base'; import objectsAreEqual from '../utils/objects-are-equal'; -const { RSVP } = Ember; +const { RSVP, $: jQuery } = Ember; /** Session store that persists data in the browser's `localStorage`. @@ -25,10 +25,10 @@ export default BaseStore.extend({ @property key @type String - @default 'ember_simple_auth:session' + @default 'ember_simple_auth-session' @public */ - key: 'ember_simple_auth:session', + key: 'ember_simple_auth-session', init() { this._super(...arguments); @@ -82,7 +82,7 @@ export default BaseStore.extend({ }, _bindToStorageEvents() { - Ember.$(window).bind('storage', (e) => { + jQuery(window).bind('storage', (e) => { if (e.originalEvent.key === this.key) { this.restore().then((data) => { if (!objectsAreEqual(data, this._lastData)) { diff --git a/bower.json b/bower.json index 50aa0c6e1..ede1f84ad 100644 --- a/bower.json +++ b/bower.json @@ -2,12 +2,12 @@ "name": "ember-simple-auth", "dependencies": { "bootstrap": "~3.3.6", - "ember": "~2.4.1", - "ember-cli-shims": "0.1.0", + "ember": "~2.6.0", + "ember-cli-shims": "0.1.1", "ember-cli-test-loader": "0.2.2", - "ember-mocha": "~0.8.7", + "ember-mocha": "~0.8.11", "sinonjs": "~1.17.1", - "base64": "~0.3.0", - "pretender": "^0.12.0" + "base64": "~1.0.0", + "pretender": "~1.1.0" } } diff --git a/ember-cli-build.js b/ember-cli-build.js index 37380101b..aec17b9dc 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -3,8 +3,6 @@ const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); const yuidoc = require('broccoli-yuidoc'); const version = require('git-repo-version')(); const Handlebars = require('handlebars'); -const mergeTrees = require('broccoli-merge-trees'); -const merge = require('lodash/merge'); var sourceTrees = []; @@ -30,8 +28,6 @@ module.exports = function(defaults) { app.import('bower_components/bootstrap/dist/css/bootstrap.css'); - sourceTrees.push(app.toTree()); - const yuidocTree = new yuidoc(['addon', 'app'], { destDir: 'docs', yuidoc: { @@ -51,5 +47,5 @@ module.exports = function(defaults) { sourceTrees.push(yuidocTree); } - return mergeTrees(sourceTrees); + return app.toTree(sourceTrees); }; diff --git a/package.json b/package.json index 8235f75c9..eddd566b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-simple-auth", - "version": "1.1.0-beta.4", + "version": "1.1.0", "description": "A lightweight library for implementing authentication/authorization with Ember.js applications.", "directories": { "doc": "doc", @@ -8,9 +8,9 @@ }, "scripts": { "build": "ember build", - "fastboot": "ember fastboot --serve-assets", + "fastboot": "echo \"Please also run npm start\"; ember fastboot --serve-assets", "start": "ember server", - "test": "ember try:testall", + "test": "ember try:each", "nodetest": "node node-tests/nodetest-runner.js" }, "repository": "https://github.com/simplabs/ember-simple-auth", @@ -20,60 +20,59 @@ "author": "simplabs GmbH", "license": "MIT", "devDependencies": { - "body-parser": "^1.2.0", - "broccoli-asset-rev": "^2.2.0", + "body-parser": "1.15.2", + "broccoli-asset-rev": "^2.4.2", "broccoli-merge-trees": "^1.1.1", "broccoli-yuidoc": "~2.1.0", "cors": "~2.7.1", - "ember-ajax": "0.7.1", - "ember-browserify": "~1.1.7", - "ember-cli": "2.4.2", + "ember-ajax": "^2.0.1", + "ember-browserify": "1.1.9", + "ember-cli": "2.6.2", "ember-cli-app-version": "^1.0.0", "ember-cli-base64": "~0.0.1", - "ember-cli-blueprint-test-helpers": "~0.10.0", + "ember-cli-blueprint-test-helpers": "~0.13.0", "ember-cli-content-security-policy": "~0.5.0", "ember-cli-dependency-checker": "^1.2.0", - "ember-cli-fastboot": "0.6.2", - "ember-cli-htmlbars": "^1.0.1", + "ember-cli-fastboot": "1.0.0-beta.7", + "ember-cli-htmlbars": "^1.0.3", "ember-cli-htmlbars-inline-precompile": "^0.3.1", - "ember-cli-inject-live-reload": "^1.3.1", - "ember-cli-internal-test-helpers": "^0.7.0", - "ember-cli-mocha": "~0.10.0", - "ember-cli-pretender": "~0.5.0", - "ember-cli-release": "0.2.8", + "ember-cli-inject-live-reload": "^1.4.0", + "ember-cli-jshint": "^1.0.0", + "ember-cli-mocha": "~0.10.4", + "ember-cli-pretender": "~0.6.0", + "ember-cli-release": "^0.2.9", "ember-cli-sri": "^2.1.0", "ember-cli-uglify": "^1.2.0", - "ember-data": "2.5.0-beta.3", + "ember-data": "^2.6.0", "ember-disable-prototype-extensions": "^1.1.0", - "ember-disable-proxy-controllers": "^1.0.1", - "ember-export-application-global": "^1.0.4", - "ember-load-initializers": "^0.5.0", + "ember-export-application-global": "^1.0.5", + "ember-load-initializers": "^0.5.1", "ember-resolver": "^2.0.3", + "ember-welcome-page": "^1.0.1", "ember-sinon": "~0.5.0", - "ember-suave": "~2.0.1", - "ember-try": "^0.1.2", - "express": "^4.12.3", + "ember-suave": "~3.0.1", + "express": "^4.14.0", "git-repo-version": "^0.3.0", - "glob": "^5.0.13", + "glob": "^7.0.4", "handlebars": "~4.0.5", - "loader.js": "^4.0.0", - "lodash": "^4.5.1", + "loader.js": "^4.0.1", + "lodash": "^4.13.1", "marked": "^0.3.5", - "mocha": "^2.4.5", + "mocha": "^2.5.3", "mocha-only-detector": "0.1.0", "morgan": "^1.5.2", "rimraf": "^2.5.2", "sinon-chai": "~2.8.0", - "torii": "~0.6.1" + "torii": "~0.8.0" }, "keywords": [ "ember-addon" ], "dependencies": { - "ember-cli-babel": "^5.1.5", + "ember-cli-babel": "^5.1.6", "ember-cli-is-package-missing": "^1.0.0", - "ember-cookies": "~0.0.6", - "ember-getowner-polyfill": "1.0.0", + "ember-cookies": "0.0.7", + "ember-getowner-polyfill": "1.0.1", "silent-error": "^1.0.0" }, "ember-addon": { diff --git a/tests/acceptance/authentication-test.js b/tests/acceptance/authentication-test.js index 4ff2ec09f..c2e53a5dc 100644 --- a/tests/acceptance/authentication-test.js +++ b/tests/acceptance/authentication-test.js @@ -8,6 +8,8 @@ import { invalidateSession, authenticateSession, currentSession } from '../helpe import destroyApp from '../helpers/destroy-app'; import config from '../../config/environment'; +const { tryInvoke } = Ember; + describe('Acceptance: Authentication', function() { let application; let server; @@ -17,7 +19,7 @@ describe('Acceptance: Authentication', function() { }); afterEach(function() { - Ember.tryInvoke(server, 'shutdown'); + tryInvoke(server, 'shutdown'); destroyApp(application); }); diff --git a/tests/dummy/app/adapters/application.js b/tests/dummy/app/adapters/application.js index ffdd7f3a6..dd3030a1a 100644 --- a/tests/dummy/app/adapters/application.js +++ b/tests/dummy/app/adapters/application.js @@ -2,7 +2,9 @@ import DS from 'ember-data'; import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin'; import config from '../config/environment'; -export default DS.JSONAPIAdapter.extend(DataAdapterMixin, { +const { JSONAPIAdapter } = DS; + +export default JSONAPIAdapter.extend(DataAdapterMixin, { authorizer: 'authorizer:application', host: config.apiHost }); diff --git a/tests/dummy/app/app.js b/tests/dummy/app/app.js index 831ad6106..570bcc410 100644 --- a/tests/dummy/app/app.js +++ b/tests/dummy/app/app.js @@ -3,11 +3,13 @@ import Resolver from './resolver'; import loadInitializers from 'ember-load-initializers'; import config from './config/environment'; +const { Application } = Ember; + let App; Ember.MODEL_FACTORY_INJECTIONS = true; -App = Ember.Application.extend({ +App = Application.extend({ modulePrefix: config.modulePrefix, podModulePrefix: config.podModulePrefix, Resolver diff --git a/tests/dummy/app/authenticators/oauth2.js b/tests/dummy/app/authenticators/oauth2.js index 0faffeadc..bdf6d7c62 100644 --- a/tests/dummy/app/authenticators/oauth2.js +++ b/tests/dummy/app/authenticators/oauth2.js @@ -1,5 +1,7 @@ import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant'; +import config from '../config/environment'; export default OAuth2PasswordGrant.extend({ - serverTokenRevocationEndpoint: '/revoke' + serverTokenEndpoint: `${config.apiHost}/token`, + serverTokenRevocationEndpoint: `${config.apiHost}/revoke` }); diff --git a/tests/dummy/app/components/login-form.js b/tests/dummy/app/components/login-form.js index 494b9adc0..4c8931241 100644 --- a/tests/dummy/app/components/login-form.js +++ b/tests/dummy/app/components/login-form.js @@ -1,8 +1,8 @@ import Ember from 'ember'; -const { service } = Ember.inject; +const { inject: { service }, Component } = Ember; -export default Ember.Component.extend({ +export default Component.extend({ session: service('session'), actions: { diff --git a/tests/dummy/app/components/main-navigation.js b/tests/dummy/app/components/main-navigation.js index de1267517..d92e1d310 100644 --- a/tests/dummy/app/components/main-navigation.js +++ b/tests/dummy/app/components/main-navigation.js @@ -1,8 +1,8 @@ import Ember from 'ember'; -const { service } = Ember.inject; +const { inject: { service }, Component } = Ember; -export default Ember.Component.extend({ +export default Component.extend({ session: service('session'), sessionAccount: service('session-account'), diff --git a/tests/dummy/app/controllers/application.js b/tests/dummy/app/controllers/application.js index 975d42ad0..0cb67c821 100644 --- a/tests/dummy/app/controllers/application.js +++ b/tests/dummy/app/controllers/application.js @@ -1,6 +1,8 @@ import Ember from 'ember'; -export default Ember.Controller.extend({ +const { Controller } = Ember; + +export default Controller.extend({ actions: { transitionToLoginRoute() { this.transitionToRoute('login'); diff --git a/tests/dummy/app/models/account.js b/tests/dummy/app/models/account.js index 6c5c70c6b..25b3861b5 100644 --- a/tests/dummy/app/models/account.js +++ b/tests/dummy/app/models/account.js @@ -1,8 +1,8 @@ import DS from 'ember-data'; -const { attr } = DS; +const { attr, Model } = DS; -export default DS.Model.extend({ +export default Model.extend({ login: attr('string'), name: attr('string') }); diff --git a/tests/dummy/app/models/post.js b/tests/dummy/app/models/post.js index 7f7195e0f..ac4e97d9b 100644 --- a/tests/dummy/app/models/post.js +++ b/tests/dummy/app/models/post.js @@ -1,8 +1,8 @@ import DS from 'ember-data'; -const { attr } = DS; +const { attr, Model } = DS; -export default DS.Model.extend({ +export default Model.extend({ title: attr('string'), body: attr('string') }); diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 4a993a405..7ed50d074 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -1,7 +1,9 @@ import Ember from 'ember'; import config from './config/environment'; -const Router = Ember.Router.extend({ +const { Router: EmberRouter } = Ember; + +const Router = EmberRouter.extend({ location: config.locationType }); diff --git a/tests/dummy/app/routes/application.js b/tests/dummy/app/routes/application.js index 79367dd26..365ed9854 100644 --- a/tests/dummy/app/routes/application.js +++ b/tests/dummy/app/routes/application.js @@ -1,9 +1,9 @@ import Ember from 'ember'; import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin'; -const { service } = Ember.inject; +const { inject: { service }, Route } = Ember; -export default Ember.Route.extend(ApplicationRouteMixin, { +export default Route.extend(ApplicationRouteMixin, { sessionAccount: service('session-account'), beforeModel() { diff --git a/tests/dummy/app/routes/auth-error.js b/tests/dummy/app/routes/auth-error.js index 23ffd770a..0bc387ca0 100644 --- a/tests/dummy/app/routes/auth-error.js +++ b/tests/dummy/app/routes/auth-error.js @@ -1,7 +1,9 @@ import Ember from 'ember'; import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; -export default Ember.Route.extend(AuthenticatedRouteMixin, { +const { Route } = Ember; + +export default Route.extend(AuthenticatedRouteMixin, { model() { return this.get('store').find('post', 3); } diff --git a/tests/dummy/app/routes/login.js b/tests/dummy/app/routes/login.js index f81116f6c..ab594ec79 100644 --- a/tests/dummy/app/routes/login.js +++ b/tests/dummy/app/routes/login.js @@ -1,4 +1,6 @@ import Ember from 'ember'; import UnauthenticatedRouteMixin from 'ember-simple-auth/mixins/unauthenticated-route-mixin'; -export default Ember.Route.extend(UnauthenticatedRouteMixin); +const { Route } = Ember; + +export default Route.extend(UnauthenticatedRouteMixin); diff --git a/tests/dummy/app/routes/protected.js b/tests/dummy/app/routes/protected.js index fe01e2f73..cc9f120b7 100644 --- a/tests/dummy/app/routes/protected.js +++ b/tests/dummy/app/routes/protected.js @@ -1,7 +1,9 @@ import Ember from 'ember'; import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; -export default Ember.Route.extend(AuthenticatedRouteMixin, { +const { Route } = Ember; + +export default Route.extend(AuthenticatedRouteMixin, { model() { return this.get('store').findAll('post'); } diff --git a/tests/dummy/app/services/session-account.js b/tests/dummy/app/services/session-account.js index ec2de46a9..fc029da73 100644 --- a/tests/dummy/app/services/session-account.js +++ b/tests/dummy/app/services/session-account.js @@ -1,15 +1,15 @@ import Ember from 'ember'; -const { inject: { service }, RSVP } = Ember; +const { inject: { service }, RSVP, Service, isEmpty } = Ember; -export default Ember.Service.extend({ +export default Service.extend({ session: service('session'), store: service(), loadCurrentUser() { return new RSVP.Promise((resolve, reject) => { const accountId = this.get('session.data.authenticated.account_id'); - if (!Ember.isEmpty(accountId)) { + if (!isEmpty(accountId)) { return this.get('store').find('account', accountId).then((account) => { this.set('account', account); resolve(); diff --git a/tests/helpers/destroy-app.js b/tests/helpers/destroy-app.js index c3d4d1abb..926d7563e 100644 --- a/tests/helpers/destroy-app.js +++ b/tests/helpers/destroy-app.js @@ -1,5 +1,7 @@ import Ember from 'ember'; +const { run } = Ember; + export default function destroyApp(application) { - Ember.run(application, 'destroy'); + run(application, 'destroy'); } diff --git a/tests/helpers/module-for-acceptance.js b/tests/helpers/module-for-acceptance.js index 21e0216e7..b3f2117a2 100644 --- a/tests/helpers/module-for-acceptance.js +++ b/tests/helpers/module-for-acceptance.js @@ -1,7 +1,10 @@ import { module } from 'qunit'; +import Ember from 'ember'; import startApp from '../helpers/start-app'; import destroyApp from '../helpers/destroy-app'; +const { RSVP: { Promise } } = Ember; + export default function(name, options = {}) { module(name, { beforeEach() { @@ -9,19 +12,16 @@ export default function(name, options = {}) { if (options.beforeEach) { // jscs:disable requireSpread - options.beforeEach.apply(this, arguments); + return options.beforeEach.apply(this, arguments); // jscs:enable requireSpread } }, afterEach() { - if (options.afterEach) { - // jscs:disable requireSpread - options.afterEach.apply(this, arguments); - // jscs:enable requireSpread - } - - destroyApp(this.application); + // jscs:disable requireSpread + let afterEach = options.afterEach && options.afterEach.apply(this, arguments); + // jscs:enable requireSpread + return Promise.resolve(afterEach).then(() => destroyApp(this.application)); } }); } diff --git a/tests/helpers/start-app.js b/tests/helpers/start-app.js index e098f1d5b..83f842c52 100644 --- a/tests/helpers/start-app.js +++ b/tests/helpers/start-app.js @@ -2,13 +2,15 @@ import Ember from 'ember'; import Application from '../../app'; import config from '../../config/environment'; +const { merge, run } = Ember; + export default function startApp(attrs) { let application; - let attributes = Ember.merge({}, config.APP); - attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; + let attributes = merge({}, config.APP); + attributes = merge(attributes, attrs); // use defaults, but you can override; - Ember.run(() => { + run(() => { application = Application.create(attributes); application.setupForTesting(); application.injectTestHelpers(); diff --git a/tests/unit/authenticators/devise-test.js b/tests/unit/authenticators/devise-test.js index 501e535f4..8174eae2d 100644 --- a/tests/unit/authenticators/devise-test.js +++ b/tests/unit/authenticators/devise-test.js @@ -4,33 +4,25 @@ import { it } from 'ember-mocha'; import { describe, beforeEach, afterEach } from 'mocha'; import { expect } from 'chai'; import sinon from 'sinon'; +import Pretender from 'pretender'; import Devise from 'ember-simple-auth/authenticators/devise'; +const { $: jQuery, run: { next }, tryInvoke } = Ember; + describe('DeviseAuthenticator', () => { - let xhr; let server; let authenticator; beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - server = sinon.fakeServer.create(); - server.autoRespond = true; - authenticator = Devise.create(); + server = new Pretender(); + authenticator = Devise.create(); }); afterEach(() => { - xhr.restore(); + tryInvoke(server, 'shutdown'); }); describe('#restore', () => { - beforeEach(() => { - server.respondWith('POST', '/users/sign_in', [ - 201, - { 'Content-Type': 'application/json' }, - '{ "user_token": "secret token!" }' - ]); - }); - describe('when the data contains a token and email', () => { it('resolves with the correct data', (done) => { authenticator.restore({ token: 'secret token!', email: 'user@email.com' }).then((content) => { @@ -45,10 +37,9 @@ describe('DeviseAuthenticator', () => { authenticator = Devise.extend({ tokenAttributeName: 'employee.token', identificationAttributeName: 'employee.email' }).create(); }); - it('resolves with the correct data', (done) => { - authenticator.restore({ employee: { token: 'secret token!', email: 'user@email.com' } }).then((content) => { + it('resolves with the correct data', () => { + return authenticator.restore({ employee: { token: 'secret token!', email: 'user@email.com' } }).then((content) => { expect(content).to.eql({ employee: { token: 'secret token!', email: 'user@email.com' } }); - done(); }); }); }); @@ -56,18 +47,18 @@ describe('DeviseAuthenticator', () => { describe('#authenticate', () => { beforeEach(() => { - sinon.spy(Ember.$, 'ajax'); + sinon.spy(jQuery, 'ajax'); }); afterEach(() => { - Ember.$.ajax.restore(); + jQuery.ajax.restore(); }); it('sends an AJAX request to the sign in endpoint', (done) => { authenticator.authenticate('identification', 'password'); - Ember.run.next(() => { - let [args] = Ember.$.ajax.getCall(0).args; + next(() => { + let [args] = jQuery.ajax.getCall(0).args; delete args.beforeSend; expect(args).to.eql({ url: '/users/sign_in', @@ -81,29 +72,39 @@ describe('DeviseAuthenticator', () => { describe('when the authentication request is successful', () => { beforeEach(() => { - server.respondWith('POST', '/users/sign_in', [ - 201, - { 'Content-Type': 'application/json' }, - '{ "access_token": "secret token!" }' - ]); + server.post('/users/sign_in', () => [201, { 'Content-Type': 'application/json' }, '{ "token": "secret token!", "email": "email@address.com" }']); }); it('resolves with the correct data', (done) => { authenticator.authenticate('email@address.com', 'password').then((data) => { expect(true).to.be.true; - expect(data).to.eql({ 'access_token': 'secret token!' }); + expect(data).to.eql({ token: 'secret token!', email: 'email@address.com' }); done(); }); }); + + describe('when the server returns incomplete data', () => { + it('fails when token is missing', () => { + server.post('/users/sign_in', () => [201, { 'Content-Type': 'application/json' }, '{ "email": "email@address.com" }']); + + return authenticator.authenticate('email@address.com', 'password').catch((error) => { + expect(error).to.eql('Check that server response includes token and email'); + }); + }); + + it('fails when identification is missing', () => { + server.post('/users/sign_in', () => [201, { 'Content-Type': 'application/json' }, '{ "token": "secret token!" }']); + + return authenticator.authenticate('email@address.com', 'password').catch((error) => { + expect(error).to.eql('Check that server response includes token and email'); + }); + }); + }); }); describe('when the authentication request fails', () => { beforeEach(() => { - server.respondWith('POST', '/users/sign_in', [ - 400, - { 'Content-Type': 'application/json' }, - '{ "error": "invalid_grant" }' - ]); + server.post('/users/sign_in', () => [400, { 'Content-Type': 'application/json', 'X-Custom-Context': 'foobar' }, '{ "error": "invalid_grant" }']); }); it('rejects with the correct error', (done) => { @@ -112,6 +113,24 @@ describe('DeviseAuthenticator', () => { done(); }); }); + + describe('when reject with XHR is enabled', () => { + beforeEach(() => { + authenticator.set('rejectWithXhr', true); + }); + + it('rejects with xhr object', () => { + return authenticator.authenticate('username', 'password').catch((error) => { + expect(error.responseJSON).to.eql({ error: 'invalid_grant' }); + }); + }); + + it('provides access to custom headers', () => { + return authenticator.authenticate('username', 'password').catch((error) => { + expect(error.getResponseHeader('X-Custom-Context')).to.eql('foobar'); + }); + }); + }); }); it('can customize the ajax request', (done) => { @@ -121,12 +140,24 @@ describe('DeviseAuthenticator', () => { } }).create(); authenticator.authenticate('identification', 'password'); - Ember.run.next(() => { - let [args] = Ember.$.ajax.getCall(0).args; + + next(() => { + let [args] = jQuery.ajax.getCall(0).args; expect(args.contentType).to.eql('application/json'); done(); }); }); + + it('can handle a resp with the namespace of the resource name', (done) => { + server.post('/users/sign_in', () => [201, { 'Content-Type': 'application/json' }, '{ "user": { "token": "secret token!", "email": "email@address.com" } }']); + + authenticator.authenticate('email@address.com', 'password').then((data) => { + expect(true).to.be.true; + expect(data).to.eql({ token: 'secret token!', email: 'email@address.com' }); + done(); + }); + }); + }); describe('#invalidate', () => { diff --git a/tests/unit/authenticators/oauth2-password-grant-test.js b/tests/unit/authenticators/oauth2-password-grant-test.js index 5f713679d..92cb86fd6 100644 --- a/tests/unit/authenticators/oauth2-password-grant-test.js +++ b/tests/unit/authenticators/oauth2-password-grant-test.js @@ -5,24 +5,24 @@ import { it } from 'ember-mocha'; import { describe, beforeEach, afterEach } from 'mocha'; import { expect } from 'chai'; import sinon from 'sinon'; +import Pretender from 'pretender'; import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant'; +const { $: jQuery, run: { next }, tryInvoke } = Ember; + describe('OAuth2PasswordGrantAuthenticator', () => { let authenticator; - let xhr; let server; beforeEach(() => { - authenticator = OAuth2PasswordGrant.create(); - xhr = sinon.useFakeXMLHttpRequest(); - server = sinon.fakeServer.create(); - server.autoRespond = true; - sinon.spy(Ember.$, 'ajax'); + authenticator = OAuth2PasswordGrant.create(); + server = new Pretender(); + sinon.spy(jQuery, 'ajax'); }); afterEach(() => { - xhr.restore(); - Ember.$.ajax.restore(); + tryInvoke(server, 'shutdown'); + jQuery.ajax.restore(); }); describe('#restore', () => { @@ -38,11 +38,7 @@ describe('OAuth2PasswordGrantAuthenticator', () => { describe('when automatic token refreshing is enabled', () => { describe('when the refresh request is successful', () => { beforeEach(() => { - server.respondWith('POST', '/token', [ - 200, - { 'Content-Type': 'application/json' }, - '{ "access_token": "secret token 2!", "expires_in": 67890, "refresh_token": "refresh token 2!" }' - ]); + server.post('/token', () => [200, { 'Content-Type': 'application/json' }, '{ "access_token": "secret token 2!", "expires_in": 67890, "refresh_token": "refresh token 2!" }']); }); it('resolves with the correct data', (done) => { @@ -105,8 +101,8 @@ describe('OAuth2PasswordGrantAuthenticator', () => { it('sends an AJAX request to the token endpoint', (done) => { authenticator.authenticate('username', 'password'); - Ember.run.next(() => { - expect(Ember.$.ajax.getCall(0).args[0]).to.eql({ + next(() => { + expect(jQuery.ajax.getCall(0).args[0]).to.eql({ url: '/token', type: 'POST', data: { 'grant_type': 'password', username: 'username', password: 'password' }, @@ -121,8 +117,8 @@ describe('OAuth2PasswordGrantAuthenticator', () => { authenticator.set('clientId', 'test-client'); authenticator.authenticate('username', 'password'); - Ember.run.next(() => { - expect(Ember.$.ajax.getCall(0).args[0]).to.eql({ + next(() => { + expect(jQuery.ajax.getCall(0).args[0]).to.eql({ url: '/token', type: 'POST', data: { 'grant_type': 'password', username: 'username', password: 'password' }, @@ -134,11 +130,27 @@ describe('OAuth2PasswordGrantAuthenticator', () => { }); }); + it('sends an AJAX request to the token endpoint with customized headers', function(done) { + authenticator.authenticate('username', 'password', [], { 'X-Custom-Context': 'foobar' }); + + next(() => { + expect(jQuery.ajax.getCall(0).args[0]).to.eql({ + url: '/token', + type: 'POST', + data: { 'grant_type': 'password', username: 'username', password: 'password' }, + dataType: 'json', + contentType: 'application/x-www-form-urlencoded', + headers: { 'X-Custom-Context': 'foobar' } + }); + done(); + }); + }); + it('sends a single OAuth scope to the token endpoint', function(done) { authenticator.authenticate('username', 'password', 'public'); - Ember.run.next(() => { - expect(Ember.$.ajax.getCall(0).args[0].data.scope).to.eql('public'); + next(() => { + expect(jQuery.ajax.getCall(0).args[0].data.scope).to.eql('public'); done(); }); }); @@ -146,22 +158,19 @@ describe('OAuth2PasswordGrantAuthenticator', () => { it('sends multiple OAuth scopes to the token endpoint', (done) => { authenticator.authenticate('username', 'password', ['public', 'private']); - Ember.run.next(() => { - expect(Ember.$.ajax.getCall(0).args[0].data.scope).to.eql('public private'); + next(() => { + expect(jQuery.ajax.getCall(0).args[0].data.scope).to.eql('public private'); done(); }); }); describe('when the authentication request is successful', () => { beforeEach(() => { - server.respondWith('POST', '/token', [ - 200, - { 'Content-Type': 'application/json' }, - '{ "access_token": "secret token!" }' - ]); + server.post('/token', () => [200, { 'Content-Type': 'application/json' }, '{ "access_token": "secret token!" }']); }); it('resolves with the correct data', (done) => { + authenticator.set('refreshAccessTokens', false); authenticator.authenticate('username', 'password').then((data) => { expect(true).to.be.true; expect(data).to.eql({ 'access_token': 'secret token!' }); @@ -171,11 +180,7 @@ describe('OAuth2PasswordGrantAuthenticator', () => { describe('when the server response includes expiration data', () => { beforeEach(() => { - server.respondWith('POST', '/token', [ - 200, - { 'Content-Type': 'application/json' }, - '{ "access_token": "secret token!", "expires_in": 12345, "refresh_token": "refresh token!" }' - ]); + server.post('/token', () => [200, { 'Content-Type': 'application/json' }, '{ "access_token": "secret token!", "expires_in": 12345, "refresh_token": "refresh token!" }']); }); it('resolves with the correct data', (done) => { @@ -187,15 +192,21 @@ describe('OAuth2PasswordGrantAuthenticator', () => { }); }); }); + + describe('when the server returns incomplete data', () => { + it('fails when no access_token is present', () => { + server.post('/token', () => [200, { 'Content-Type': 'application/json' }, '{}']); + + return authenticator.authenticate('username', 'password').catch((error) => { + expect(error).to.eql('access_token is missing in server response'); + }); + }); + }); }); describe('when the authentication request fails', () => { beforeEach(() => { - server.respondWith('POST', '/token', [ - 400, - { 'Content-Type': 'application/json' }, - '{ "error": "invalid_grant" }' - ]); + server.post('/token', () => [400, { 'Content-Type': 'application/json', 'X-Custom-Context': 'foobar' }, '{ "error": "invalid_grant" }']); }); it('rejects with the correct error', (done) => { @@ -204,6 +215,24 @@ describe('OAuth2PasswordGrantAuthenticator', () => { done(); }); }); + + describe('when reject with XHR is enabled', () => { + beforeEach(() => { + authenticator.set('rejectWithXhr', true); + }); + + it('rejects with xhr object', () => { + return authenticator.authenticate('username', 'password').catch((error) => { + expect(error.responseJSON).to.eql({ error: 'invalid_grant' }); + }); + }); + + it('provides access to custom headers', () => { + return authenticator.authenticate('username', 'password').catch((error) => { + expect(error.getResponseHeader('X-Custom-Context')).to.eql('foobar'); + }); + }); + }); }); }); @@ -225,8 +254,8 @@ describe('OAuth2PasswordGrantAuthenticator', () => { it('sends an AJAX request to the revokation endpoint', (done) => { authenticator.invalidate({ 'access_token': 'access token!' }); - Ember.run.next(() => { - expect(Ember.$.ajax.getCall(0).args[0]).to.eql({ + next(() => { + expect(jQuery.ajax.getCall(0).args[0]).to.eql({ url: '/revoke', type: 'POST', data: { 'token_type_hint': 'access_token', token: 'access token!' }, @@ -239,7 +268,7 @@ describe('OAuth2PasswordGrantAuthenticator', () => { describe('when the revokation request is successful', () => { beforeEach(() => { - server.respondWith('POST', '/revoke', [200, { 'Content-Type': 'application/json' }, '']); + server.post('/revoke', () => [200, {}, '']); }); itSuccessfullyInvalidatesTheSession(); @@ -247,8 +276,7 @@ describe('OAuth2PasswordGrantAuthenticator', () => { describe('when the revokation request fails', () => { beforeEach(() => { - server.respondWith('POST', '/revoke', [400, { 'Content-Type': 'application/json' }, - '{ "error": "unsupported_grant_type" }']); + server.post('/token', () => [400, { 'Content-Type': 'application/json' }, '{ "error": "unsupported_grant_type" }']); }); itSuccessfullyInvalidatesTheSession(); @@ -258,8 +286,8 @@ describe('OAuth2PasswordGrantAuthenticator', () => { it('sends an AJAX request to invalidate the refresh token', (done) => { authenticator.invalidate({ 'access_token': 'access token!', 'refresh_token': 'refresh token!' }); - Ember.run.next(() => { - expect(Ember.$.ajax.getCall(1).args[0]).to.eql({ + next(() => { + expect(jQuery.ajax.getCall(1).args[0]).to.eql({ url: '/revoke', type: 'POST', data: { 'token_type_hint': 'refresh_token', token: 'refresh token!' }, @@ -277,13 +305,21 @@ describe('OAuth2PasswordGrantAuthenticator', () => { }); }); + describe('#tokenRefreshOffset', () => { + it('returns a number between 5000 and 10000', (done) => { + expect(authenticator.get('tokenRefreshOffset')).to.be.at.least(5000); + expect(authenticator.get('tokenRefreshOffset')).to.be.below(10000); + done(); + }); + }); + // testing private API here ;( describe('#_refreshAccessToken', () => { it('sends an AJAX request to the token endpoint', (done) => { authenticator._refreshAccessToken(12345, 'refresh token!'); - Ember.run.next(() => { - expect(Ember.$.ajax.getCall(0).args[0]).to.eql({ + next(() => { + expect(jQuery.ajax.getCall(0).args[0]).to.eql({ url: '/token', type: 'POST', data: { 'grant_type': 'refresh_token', 'refresh_token': 'refresh token!' }, @@ -296,11 +332,7 @@ describe('OAuth2PasswordGrantAuthenticator', () => { describe('when the refresh request is successful', () => { beforeEach(() => { - server.respondWith('POST', '/token', [ - 200, - { 'Content-Type': 'application/json' }, - '{ "access_token": "secret token 2!" }' - ]); + server.post('/token', () => [200, { 'Content-Type': 'application/json' }, '{ "access_token": "secret token 2!" }']); }); it('triggers the "sessionDataUpdated" event', (done) => { @@ -316,11 +348,7 @@ describe('OAuth2PasswordGrantAuthenticator', () => { describe('when the server reponse includes updated expiration data', () => { beforeEach(() => { - server.respondWith('POST', '/token', [ - 200, - { 'Content-Type': 'application/json' }, - '{ "access_token": "secret token 2!", "expires_in": 67890, "refresh_token": "refresh token 2!" }' - ]); + server.post('/token', () => [200, { 'Content-Type': 'application/json' }, '{ "access_token": "secret token 2!", "expires_in": 67890, "refresh_token": "refresh token 2!" }']); }); it('triggers the "sessionDataUpdated" event with the correct data', (done) => { diff --git a/tests/unit/authenticators/torii-test.js b/tests/unit/authenticators/torii-test.js index a13faf0f3..ad81ccd20 100644 --- a/tests/unit/authenticators/torii-test.js +++ b/tests/unit/authenticators/torii-test.js @@ -6,6 +6,8 @@ import { expect } from 'chai'; import sinon from 'sinon'; import Torii from 'ember-simple-auth/authenticators/torii'; +const { RSVP } = Ember; + describe('ToriiAuthenticator', () => { let authenticator; let torii; @@ -39,7 +41,7 @@ describe('ToriiAuthenticator', () => { describe('when there is a torii provider in the session data', () => { describe('when torii fetches successfully', () => { beforeEach(() => { - sinon.stub(torii, 'fetch').returns(Ember.RSVP.resolve({ some: 'other data' })); + sinon.stub(torii, 'fetch').returns(RSVP.resolve({ some: 'other data' })); }); it('returns a promise that resolves with the session data', () => { @@ -51,7 +53,7 @@ describe('ToriiAuthenticator', () => { describe('when torii does not fetch successfully', () => { beforeEach(() => { - sinon.stub(torii, 'fetch').returns(Ember.RSVP.reject()); + sinon.stub(torii, 'fetch').returns(RSVP.reject()); }); itDoesNotRestore({ some: 'data', provider: 'provider' }); @@ -74,7 +76,7 @@ describe('ToriiAuthenticator', () => { describe('when torii opens successfully', () => { beforeEach(() => { - sinon.stub(torii, 'open').returns(Ember.RSVP.resolve({ some: 'data' })); + sinon.stub(torii, 'open').returns(RSVP.resolve({ some: 'data' })); }); it('returns a promise that resolves with the session data', () => { @@ -86,7 +88,7 @@ describe('ToriiAuthenticator', () => { describe('when torii does not open successfully', () => { beforeEach(() => { - sinon.stub(torii, 'open').returns(Ember.RSVP.reject()); + sinon.stub(torii, 'open').returns(RSVP.reject()); }); it('returns a rejecting promise', () => { @@ -100,7 +102,7 @@ describe('ToriiAuthenticator', () => { describe('#invalidate', () => { describe('when torii closes successfully', () => { beforeEach(() => { - sinon.stub(torii, 'close').returns(Ember.RSVP.resolve()); + sinon.stub(torii, 'close').returns(RSVP.resolve()); }); it('returns a resolving promise', () => { @@ -112,7 +114,7 @@ describe('ToriiAuthenticator', () => { describe('when torii does not close successfully', () => { beforeEach(() => { - sinon.stub(torii, 'close').returns(Ember.RSVP.reject()); + sinon.stub(torii, 'close').returns(RSVP.reject()); }); it('returns a rejecting promise', () => { diff --git a/tests/unit/instance-initializers/setup-session-restoration-test.js b/tests/unit/instance-initializers/setup-session-restoration-test.js index bf4867315..9a0df5d3d 100644 --- a/tests/unit/instance-initializers/setup-session-restoration-test.js +++ b/tests/unit/instance-initializers/setup-session-restoration-test.js @@ -6,6 +6,8 @@ import { expect } from 'chai'; import sinon from 'sinon'; import setupSessionRestoration from 'ember-simple-auth/instance-initializers/setup-session-restoration'; +const { Route, RSVP } = Ember; + describe('setupSessionRestoration', () => { let container; let containerStub; @@ -16,8 +18,7 @@ describe('setupSessionRestoration', () => { lookup() {} }; - const Route = Ember.Route.extend(); - route = Route.create(); + route = Route.extend().create(); containerStub = sinon.stub(container, 'lookup'); }); @@ -37,12 +38,11 @@ describe('setupSessionRestoration', () => { restore() {} }; - const Route = Ember.Route.extend({ + route = Route.extend({ beforeModel() { - return Ember.RSVP.resolve('test'); + return RSVP.resolve('test'); } - }); - route = Route.create(); + }).create(); containerStub.withArgs('route:application').returns(route); containerStub.withArgs('session:main').returns(session); @@ -51,7 +51,7 @@ describe('setupSessionRestoration', () => { describe('when session restoration resolves', () => { beforeEach(() => { - sinon.stub(session, 'restore').returns(Ember.RSVP.resolve()); + sinon.stub(session, 'restore').returns(RSVP.resolve()); }); it('returns the return value of the original "beforeModel" method', () => { @@ -63,7 +63,7 @@ describe('setupSessionRestoration', () => { describe('when session restoration rejects', () => { beforeEach(() => { - sinon.stub(session, 'restore').returns(Ember.RSVP.reject()); + sinon.stub(session, 'restore').returns(RSVP.reject()); }); it('returns the return value of the original "beforeModel" method', () => { diff --git a/tests/unit/internal-session-test.js b/tests/unit/internal-session-test.js index 598dad6bd..5e4f71053 100644 --- a/tests/unit/internal-session-test.js +++ b/tests/unit/internal-session-test.js @@ -8,6 +8,8 @@ import InternalSession from 'ember-simple-auth/internal-session'; import EphemeralStore from 'ember-simple-auth/session-stores/ephemeral'; import Authenticator from 'ember-simple-auth/authenticators/base'; +const { RSVP, K, run: { next } } = Ember; + describe('InternalSession', () => { let session; let store; @@ -37,7 +39,7 @@ describe('InternalSession', () => { it('stores the data the event is triggered with in its authenticated section', (done) => { authenticator.trigger('sessionDataUpdated', { some: 'property' }); - Ember.run.next(() => { + next(() => { expect(session.get('authenticated')).to.eql({ some: 'property', authenticator: 'authenticator' }); done(); }); @@ -52,7 +54,7 @@ describe('InternalSession', () => { it('is not authenticated', (done) => { authenticator.trigger('sessionDataInvalidated'); - Ember.run.next(() => { + next(() => { expect(session.get('isAuthenticated')).to.be.false; done(); }); @@ -62,7 +64,7 @@ describe('InternalSession', () => { session.set('content', { some: 'property', authenticated: { some: 'other property' } }); authenticator.trigger('sessionDataInvalidated'); - Ember.run.next(() => { + next(() => { expect(session.get('content')).to.eql({ some: 'property', authenticated: {} }); done(); }); @@ -71,7 +73,7 @@ describe('InternalSession', () => { it('updates the store', (done) => { authenticator.trigger('sessionDataInvalidated'); - Ember.run.next(() => { + next(() => { store.restore().then((properties) => { expect(properties.authenticated).to.eql({}); done(); @@ -86,7 +88,7 @@ describe('InternalSession', () => { }); authenticator.trigger('sessionDataInvalidated'); - Ember.run.next(() => { + next(() => { expect(triggered).to.be.true; done(); }); @@ -124,7 +126,7 @@ describe('InternalSession', () => { describe('when the authenticator resolves restoration', () => { beforeEach(() => { - sinon.stub(authenticator, 'restore').returns(Ember.RSVP.resolve({ some: 'property' })); + sinon.stub(authenticator, 'restore').returns(RSVP.resolve({ some: 'property' })); }); it('returns a resolving promise', () => { @@ -187,7 +189,7 @@ describe('InternalSession', () => { describe('when the authenticator rejects restoration', () => { beforeEach(() => { - sinon.stub(authenticator, 'restore').returns(Ember.RSVP.reject()); + sinon.stub(authenticator, 'restore').returns(RSVP.reject()); }); itDoesNotRestore(); @@ -200,7 +202,7 @@ describe('InternalSession', () => { describe('when the store rejects restoration', function() { beforeEach(() => { - sinon.stub(store, 'restore').returns(Ember.RSVP.Promise.reject()); + sinon.stub(store, 'restore').returns(RSVP.Promise.reject()); }); it('is not authenticated', () => { @@ -212,7 +214,7 @@ describe('InternalSession', () => { describe('when the store rejects persistance', () => { beforeEach(() => { - sinon.stub(store, 'persist').returns(Ember.RSVP.reject()); + sinon.stub(store, 'persist').returns(RSVP.reject()); }); it('is not authenticated', () => { @@ -259,7 +261,7 @@ describe('InternalSession', () => { describe('authentication', () => { describe('when the authenticator resolves authentication', () => { beforeEach(() => { - sinon.stub(authenticator, 'authenticate').returns(Ember.RSVP.resolve({ some: 'property' })); + sinon.stub(authenticator, 'authenticate').returns(RSVP.resolve({ some: 'property' })); }); it('is authenticated', () => { @@ -314,7 +316,7 @@ describe('InternalSession', () => { describe('when the authenticator rejects authentication', () => { it('is not authenticated', () => { - sinon.stub(authenticator, 'authenticate').returns(Ember.RSVP.reject('error auth')); + sinon.stub(authenticator, 'authenticate').returns(RSVP.reject('error auth')); return session.authenticate('authenticator').catch(() => { expect(session.get('isAuthenticated')).to.be.false; @@ -322,7 +324,7 @@ describe('InternalSession', () => { }); it('returns a rejecting promise', () => { - sinon.stub(authenticator, 'authenticate').returns(Ember.RSVP.reject('error auth')); + sinon.stub(authenticator, 'authenticate').returns(RSVP.reject('error auth')); return session.authenticate('authenticator').catch(() => { expect(true).to.be.true; @@ -330,7 +332,7 @@ describe('InternalSession', () => { }); it('clears its authenticated section', () => { - sinon.stub(authenticator, 'authenticate').returns(Ember.RSVP.reject('error auth')); + sinon.stub(authenticator, 'authenticate').returns(RSVP.reject('error auth')); session.set('content', { some: 'property', authenticated: { some: 'other property' } }); return session.authenticate('authenticator').catch(() => { @@ -339,7 +341,7 @@ describe('InternalSession', () => { }); it('updates the store', () => { - sinon.stub(authenticator, 'authenticate').returns(Ember.RSVP.reject('error auth')); + sinon.stub(authenticator, 'authenticate').returns(RSVP.reject('error auth')); session.set('content', { some: 'property', authenticated: { some: 'other property' } }); return session.authenticate('authenticator').catch(() => { @@ -351,7 +353,7 @@ describe('InternalSession', () => { it('does not trigger the "authenticationSucceeded" event', () => { let triggered = false; - sinon.stub(authenticator, 'authenticate').returns(Ember.RSVP.reject('error auth')); + sinon.stub(authenticator, 'authenticate').returns(RSVP.reject('error auth')); session.one('authenticationSucceeded', () => triggered = true); return session.authenticate('authenticator').catch(() => { @@ -362,7 +364,7 @@ describe('InternalSession', () => { describe('when the store rejects persistance', () => { beforeEach(() => { - sinon.stub(store, 'persist').returns(Ember.RSVP.reject()); + sinon.stub(store, 'persist').returns(RSVP.reject()); }); it('is not authenticated', () => { @@ -375,13 +377,13 @@ describe('InternalSession', () => { describe('invalidation', () => { beforeEach(() => { - sinon.stub(authenticator, 'authenticate').returns(Ember.RSVP.resolve({ some: 'property' })); + sinon.stub(authenticator, 'authenticate').returns(RSVP.resolve({ some: 'property' })); return session.authenticate('authenticator'); }); describe('when the authenticator resolves invaldiation', () => { beforeEach(() => { - sinon.stub(authenticator, 'invalidate').returns(Ember.RSVP.resolve()); + sinon.stub(authenticator, 'invalidate').returns(RSVP.resolve()); }); it('is not authenticated', () => { @@ -426,7 +428,7 @@ describe('InternalSession', () => { describe('when the authenticator rejects invalidation', () => { it('stays authenticated', () => { - sinon.stub(authenticator, 'invalidate').returns(Ember.RSVP.reject('error')); + sinon.stub(authenticator, 'invalidate').returns(RSVP.reject('error')); return session.invalidate().catch(() => { expect(session.get('isAuthenticated')).to.be.true; @@ -434,7 +436,7 @@ describe('InternalSession', () => { }); it('returns a rejecting promise', () => { - sinon.stub(authenticator, 'invalidate').returns(Ember.RSVP.reject('error')); + sinon.stub(authenticator, 'invalidate').returns(RSVP.reject('error')); return session.invalidate().catch(() => { expect(true).to.be.true; @@ -442,7 +444,7 @@ describe('InternalSession', () => { }); it('keeps its content', () => { - sinon.stub(authenticator, 'invalidate').returns(Ember.RSVP.reject('error')); + sinon.stub(authenticator, 'invalidate').returns(RSVP.reject('error')); return session.invalidate().catch(() => { expect(session.get('authenticated')).to.eql({ some: 'property', authenticator: 'authenticator' }); @@ -450,7 +452,7 @@ describe('InternalSession', () => { }); it('does not update the store', () => { - sinon.stub(authenticator, 'invalidate').returns(Ember.RSVP.reject('error')); + sinon.stub(authenticator, 'invalidate').returns(RSVP.reject('error')); return session.invalidate().catch(() => { return store.restore().then((properties) => { @@ -460,7 +462,7 @@ describe('InternalSession', () => { }); it('does not trigger the "invalidationSucceeded" event', () => { - sinon.stub(authenticator, 'invalidate').returns(Ember.RSVP.reject('error')); + sinon.stub(authenticator, 'invalidate').returns(RSVP.reject('error')); let triggered = false; session.one('invalidationSucceeded', () => triggered = true); @@ -469,12 +471,12 @@ describe('InternalSession', () => { }); }); - itHandlesAuthenticatorEvents(Ember.K); + itHandlesAuthenticatorEvents(K); }); describe('when the store rejects persistance', () => { beforeEach(() => { - sinon.stub(store, 'persist').returns(Ember.RSVP.reject()); + sinon.stub(store, 'persist').returns(RSVP.reject()); }); it('rejects but is not authenticated', () => { @@ -517,86 +519,171 @@ describe('InternalSession', () => { }); describe('when the store triggers the "sessionDataUpdated" event', () => { - describe('when there is an authenticator factory in the event data', () => { - describe('when the authenticator resolves restoration', () => { - beforeEach(() => { - sinon.stub(authenticator, 'restore').returns(Ember.RSVP.resolve({ some: 'other property' })); - }); + describe('when the session is currently busy', () => { + beforeEach(() => { + sinon.stub(store, 'restore').returns(new RSVP.Promise((resolve) => { + next(() => resolve({ some: 'other property' })); + })); + }); - it('is authenticated', (done) => { - store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + it('does not process the event', (done) => { + sinon.spy(authenticator, 'restore'); + session.restore().then(done).catch(done); + store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); - Ember.run.next(() => { - expect(session.get('isAuthenticated')).to.be.true; - done(); + expect(authenticator.restore).to.not.have.been.called; + }); + }); + + describe('when the session is not currently busy', () => { + describe('when there is an authenticator factory in the event data', () => { + describe('when the authenticator resolves restoration', () => { + beforeEach(() => { + sinon.stub(authenticator, 'restore').returns(RSVP.resolve({ some: 'other property' })); }); - }); - it('stores the data the authenticator resolves with in its authenticated section', (done) => { - store.trigger('sessionDataUpdated', { some: 'property', authenticated: { authenticator: 'authenticator' } }); + it('is authenticated', (done) => { + store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); - Ember.run.next(() => { - expect(session.get('authenticated')).to.eql({ some: 'other property', authenticator: 'authenticator' }); - done(); + next(() => { + expect(session.get('isAuthenticated')).to.be.true; + done(); + }); }); - }); - - it('persists its content in the store', (done) => { - store.trigger('sessionDataUpdated', { some: 'property', authenticated: { authenticator: 'authenticator' } }); - Ember.run.next(() => { - store.restore().then((properties) => { + it('stores the data the authenticator resolves with in its authenticated section', (done) => { + store.trigger('sessionDataUpdated', { some: 'property', authenticated: { authenticator: 'authenticator' } }); - expect(properties).to.eql({ some: 'property', authenticated: { some: 'other property', authenticator: 'authenticator' } }); + next(() => { + expect(session.get('authenticated')).to.eql({ some: 'other property', authenticator: 'authenticator' }); done(); }); }); + + it('persists its content in the store', (done) => { + store.trigger('sessionDataUpdated', { some: 'property', authenticated: { authenticator: 'authenticator' } }); + + next(() => { + store.restore().then((properties) => { + + expect(properties).to.eql({ some: 'property', authenticated: { some: 'other property', authenticator: 'authenticator' } }); + done(); + }); + }); + }); + + describe('when the session is already authenticated', () => { + beforeEach(() => { + session.set('isAuthenticated', true); + }); + + it('does not trigger the "authenticationSucceeded" event', (done) => { + let triggered = false; + session.one('authenticationSucceeded', () => triggered = true); + store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + + next(() => { + expect(triggered).to.be.false; + done(); + }); + }); + }); + + describe('when the session is not already authenticated', () => { + beforeEach(() => { + session.set('isAuthenticated', false); + }); + + it('triggers the "authenticationSucceeded" event', (done) => { + let triggered = false; + session.one('authenticationSucceeded', () => triggered = true); + store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + + next(() => { + expect(triggered).to.be.true; + done(); + }); + }); + }); }); - describe('when the session is already authenticated', () => { + describe('when the authenticator rejects restoration', () => { beforeEach(() => { - session.set('isAuthenticated', true); + sinon.stub(authenticator, 'restore').returns(RSVP.reject()); }); - it('does not trigger the "authenticationSucceeded" event', (done) => { - let triggered = false; - session.one('authenticationSucceeded', () => triggered = true); + it('is not authenticated', (done) => { store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); - Ember.run.next(() => { - expect(triggered).to.be.false; + next(() => { + expect(session.get('isAuthenticated')).to.be.false; done(); }); }); - }); - describe('when the session is not already authenticated', () => { - beforeEach(() => { - session.set('isAuthenticated', false); + it('clears its authenticated section', (done) => { + session.set('content', { some: 'property', authenticated: { some: 'other property' } }); + store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + + next(() => { + expect(session.get('content')).to.eql({ some: 'other property', authenticated: {} }); + done(); + }); }); - it('triggers the "authenticationSucceeded" event', (done) => { - let triggered = false; - session.one('authenticationSucceeded', () => triggered = true); + it('updates the store', (done) => { + session.set('content', { some: 'property', authenticated: { some: 'other property' } }); store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); - Ember.run.next(() => { - expect(triggered).to.be.true; - done(); + next(() => { + store.restore().then((properties) => { + expect(properties).to.eql({ some: 'other property', authenticated: {} }); + done(); + }); }); }); - }); - }); - describe('when the authenticator rejects restoration', () => { - beforeEach(() => { - sinon.stub(authenticator, 'restore').returns(Ember.RSVP.reject()); + describe('when the session is authenticated', () => { + beforeEach(() => { + session.set('isAuthenticated', true); + }); + + it('triggers the "invalidationSucceeded" event', (done) => { + let triggered = false; + session.one('invalidationSucceeded', () => triggered = true); + store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + + next(() => { + expect(triggered).to.be.true; + done(); + }); + }); + }); + + describe('when the session is not authenticated', () => { + beforeEach(() => { + session.set('isAuthenticated', false); + }); + + it('does not trigger the "invalidationSucceeded" event', (done) => { + let triggered = false; + session.one('invalidationSucceeded', () => triggered = true); + store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + + next(() => { + expect(triggered).to.be.false; + done(); + }); + }); + }); }); + }); + describe('when there is no authenticator factory in the store', () => { it('is not authenticated', (done) => { - store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + store.trigger('sessionDataUpdated', { some: 'other property' }); - Ember.run.next(() => { + next(() => { expect(session.get('isAuthenticated')).to.be.false; done(); }); @@ -604,9 +691,9 @@ describe('InternalSession', () => { it('clears its authenticated section', (done) => { session.set('content', { some: 'property', authenticated: { some: 'other property' } }); - store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + store.trigger('sessionDataUpdated', { some: 'other property' }); - Ember.run.next(() => { + next(() => { expect(session.get('content')).to.eql({ some: 'other property', authenticated: {} }); done(); }); @@ -614,9 +701,9 @@ describe('InternalSession', () => { it('updates the store', (done) => { session.set('content', { some: 'property', authenticated: { some: 'other property' } }); - store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + store.trigger('sessionDataUpdated', { some: 'other property' }); - Ember.run.next(() => { + next(() => { store.restore().then((properties) => { expect(properties).to.eql({ some: 'other property', authenticated: {} }); done(); @@ -632,9 +719,9 @@ describe('InternalSession', () => { it('triggers the "invalidationSucceeded" event', (done) => { let triggered = false; session.one('invalidationSucceeded', () => triggered = true); - store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + store.trigger('sessionDataUpdated', { some: 'other property' }); - Ember.run.next(() => { + next(() => { expect(triggered).to.be.true; done(); }); @@ -649,9 +736,9 @@ describe('InternalSession', () => { it('does not trigger the "invalidationSucceeded" event', (done) => { let triggered = false; session.one('invalidationSucceeded', () => triggered = true); - store.trigger('sessionDataUpdated', { some: 'other property', authenticated: { authenticator: 'authenticator' } }); + store.trigger('sessionDataUpdated', { some: 'other property' }); - Ember.run.next(() => { + next(() => { expect(triggered).to.be.false; done(); }); @@ -659,73 +746,6 @@ describe('InternalSession', () => { }); }); }); - - describe('when there is no authenticator factory in the store', () => { - it('is not authenticated', (done) => { - store.trigger('sessionDataUpdated', { some: 'other property' }); - - Ember.run.next(() => { - expect(session.get('isAuthenticated')).to.be.false; - done(); - }); - }); - - it('clears its authenticated section', (done) => { - session.set('content', { some: 'property', authenticated: { some: 'other property' } }); - store.trigger('sessionDataUpdated', { some: 'other property' }); - - Ember.run.next(() => { - expect(session.get('content')).to.eql({ some: 'other property', authenticated: {} }); - done(); - }); - }); - - it('updates the store', (done) => { - session.set('content', { some: 'property', authenticated: { some: 'other property' } }); - store.trigger('sessionDataUpdated', { some: 'other property' }); - - Ember.run.next(() => { - store.restore().then((properties) => { - expect(properties).to.eql({ some: 'other property', authenticated: {} }); - done(); - }); - }); - }); - - describe('when the session is authenticated', () => { - beforeEach(() => { - session.set('isAuthenticated', true); - }); - - it('triggers the "invalidationSucceeded" event', (done) => { - let triggered = false; - session.one('invalidationSucceeded', () => triggered = true); - store.trigger('sessionDataUpdated', { some: 'other property' }); - - Ember.run.next(() => { - expect(triggered).to.be.true; - done(); - }); - }); - }); - - describe('when the session is not authenticated', () => { - beforeEach(() => { - session.set('isAuthenticated', false); - }); - - it('does not trigger the "invalidationSucceeded" event', (done) => { - let triggered = false; - session.one('invalidationSucceeded', () => triggered = true); - store.trigger('sessionDataUpdated', { some: 'other property' }); - - Ember.run.next(() => { - expect(triggered).to.be.false; - done(); - }); - }); - }); - }); }); it('does not share the content object between multiple instances', () => { diff --git a/tests/unit/mixins/application-route-mixin-test.js b/tests/unit/mixins/application-route-mixin-test.js index 43a6db460..7ba591c5b 100644 --- a/tests/unit/mixins/application-route-mixin-test.js +++ b/tests/unit/mixins/application-route-mixin-test.js @@ -9,6 +9,8 @@ import InternalSession from 'ember-simple-auth/internal-session'; import EphemeralStore from 'ember-simple-auth/session-stores/ephemeral'; import Configuration from 'ember-simple-auth/configuration'; +const { Route, run: { next } } = Ember; + describe('ApplicationRouteMixin', () => { let session; let route; @@ -16,7 +18,7 @@ describe('ApplicationRouteMixin', () => { beforeEach(() => { session = InternalSession.create({ store: EphemeralStore.create() }); - route = Ember.Route.extend(ApplicationRouteMixin, { + route = Route.extend(ApplicationRouteMixin, { transitionTo() {} }).create({ session @@ -32,7 +34,7 @@ describe('ApplicationRouteMixin', () => { it("maps the services's 'authenticationSucceeded' event into a method call", (done) => { session.trigger('authenticationSucceeded'); - Ember.run.next(() => { + next(() => { expect(route.sessionAuthenticated).to.have.been.calledOnce; done(); }); @@ -41,7 +43,7 @@ describe('ApplicationRouteMixin', () => { it("maps the services's 'invalidationSucceeded' event into a method call", (done) => { session.trigger('invalidationSucceeded'); - Ember.run.next(() => { + next(() => { expect(route.sessionInvalidated).to.have.been.calledOnce; done(); }); @@ -51,7 +53,7 @@ describe('ApplicationRouteMixin', () => { route.beforeModel(); session.trigger('authenticationSucceeded'); - Ember.run.next(() => { + next(() => { expect(route.sessionAuthenticated).to.have.been.calledOnce; done(); }); diff --git a/tests/unit/mixins/authenticated-route-mixin-test.js b/tests/unit/mixins/authenticated-route-mixin-test.js index 0c7b8f406..bbc94620d 100644 --- a/tests/unit/mixins/authenticated-route-mixin-test.js +++ b/tests/unit/mixins/authenticated-route-mixin-test.js @@ -9,6 +9,8 @@ import InternalSession from 'ember-simple-auth/internal-session'; import Configuration from 'ember-simple-auth/configuration'; import EphemeralStore from 'ember-simple-auth/session-stores/ephemeral'; +const { Mixin, RSVP, Route } = Ember; + describe('AuthenticatedRouteMixin', () => { let route; let session; @@ -16,26 +18,23 @@ describe('AuthenticatedRouteMixin', () => { describe('#beforeModel', () => { beforeEach(() => { - const MixinImplementingBeforeModel = Ember.Mixin.create({ + const MixinImplementingBeforeModel = Mixin.create({ beforeModel() { - return Ember.RSVP.resolve('upstreamReturnValue'); + return RSVP.resolve('upstreamReturnValue'); } }); - const Route = Ember.Route.extend(MixinImplementingBeforeModel, AuthenticatedRouteMixin, { - // pretend this is never FastBoot - _isFastBoot: false, - // replace actual transitionTo as the router isn't set up etc. - transitionTo() {} - }); session = InternalSession.create({ store: EphemeralStore.create() }); transition = { - abort() {}, send() {} }; - route = Route.create({ session }); - sinon.spy(transition, 'abort'); + route = Route.extend(MixinImplementingBeforeModel, AuthenticatedRouteMixin, { + // pretend this is never FastBoot + _isFastBoot: false, + // replace actual transitionTo as the router isn't set up etc. + transitionTo() {} + }).create({ session }); sinon.spy(transition, 'send'); sinon.spy(route, 'transitionTo'); }); @@ -51,12 +50,6 @@ describe('AuthenticatedRouteMixin', () => { }); }); - it('does not abort the transition', () => { - route.beforeModel(transition); - - expect(transition.abort).to.not.have.been.called; - }); - it('does not transition to the authentication route', () => { route.beforeModel(transition); @@ -69,12 +62,6 @@ describe('AuthenticatedRouteMixin', () => { expect(route.beforeModel(transition)).to.be.undefined; }); - it('aborts the transition', () => { - route.beforeModel(transition); - - expect(transition.abort).to.have.been.called; - }); - it('transitions to the authentication route', () => { route.beforeModel(transition); diff --git a/tests/unit/mixins/data-adapter-mixin-test.js b/tests/unit/mixins/data-adapter-mixin-test.js index c484afc30..8bba68247 100644 --- a/tests/unit/mixins/data-adapter-mixin-test.js +++ b/tests/unit/mixins/data-adapter-mixin-test.js @@ -6,6 +6,8 @@ import { expect } from 'chai'; import sinon from 'sinon'; import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin'; +const { Object: EmberObject } = Ember; + describe('DataAdapterMixin', () => { let adapter; let sessionService; @@ -13,12 +15,12 @@ describe('DataAdapterMixin', () => { beforeEach(() => { hash = {}; - sessionService = Ember.Object.create({ + sessionService = EmberObject.create({ authorize() {}, invalidate() {} }); - const BaseAdapter = Ember.Object.extend({ + const BaseAdapter = EmberObject.extend({ ajaxOptions() { return hash; }, diff --git a/tests/unit/mixins/unauthenticated-route-mixin-test.js b/tests/unit/mixins/unauthenticated-route-mixin-test.js index 9b254394b..6906350c3 100644 --- a/tests/unit/mixins/unauthenticated-route-mixin-test.js +++ b/tests/unit/mixins/unauthenticated-route-mixin-test.js @@ -9,6 +9,8 @@ import InternalSession from 'ember-simple-auth/internal-session'; import Configuration from 'ember-simple-auth/configuration'; import EphemeralStore from 'ember-simple-auth/session-stores/ephemeral'; +const { Mixin, RSVP, Route } = Ember; + describe('UnauthenticatedRouteMixin', () => { let route; let session; @@ -16,26 +18,23 @@ describe('UnauthenticatedRouteMixin', () => { describe('#beforeModel', () => { beforeEach(() => { - const MixinImplementingBeforeModel = Ember.Mixin.create({ + const MixinImplementingBeforeModel = Mixin.create({ beforeModel() { - return Ember.RSVP.resolve('upstreamReturnValue'); + return RSVP.resolve('upstreamReturnValue'); } }); - const Route = Ember.Route.extend(MixinImplementingBeforeModel, UnauthenticatedRouteMixin, { - // pretend this is never FastBoot - _isFastBoot: false, - // replace actual transitionTo as the router isn't set up etc. - transitionTo() {} - }); session = InternalSession.create({ store: EphemeralStore.create() }); transition = { - abort() {}, send() {} }; - route = Route.create({ session }); - sinon.spy(transition, 'abort'); + route = Route.extend(MixinImplementingBeforeModel, UnauthenticatedRouteMixin, { + // pretend this is never FastBoot + _isFastBoot: false, + // replace actual transitionTo as the router isn't set up etc. + transitionTo() {} + }).create({ session }); sinon.spy(route, 'transitionTo'); }); @@ -44,12 +43,6 @@ describe('UnauthenticatedRouteMixin', () => { session.set('isAuthenticated', true); }); - it('aborts the transition', () => { - route.beforeModel(transition); - - expect(transition.abort).to.have.been.called; - }); - it('transitions to routeIfAlreadyAuthenticated', () => { route.beforeModel(transition); @@ -62,12 +55,6 @@ describe('UnauthenticatedRouteMixin', () => { }); describe('if the session is not authenticated', () => { - it('does not abort the transition', () => { - route.beforeModel(transition); - - expect(transition.abort).to.not.have.been.called; - }); - it('does not call route transitionTo', () => { route.beforeModel(transition); diff --git a/tests/unit/services/session-test.js b/tests/unit/services/session-test.js index f61f5b934..d35309af1 100644 --- a/tests/unit/services/session-test.js +++ b/tests/unit/services/session-test.js @@ -6,13 +6,15 @@ import { expect } from 'chai'; import sinon from 'sinon'; import Session from 'ember-simple-auth/services/session'; +const { ObjectProxy, Evented, run: { next }, set } = Ember; + describe('SessionService', () => { let sessionService; let session; let authorizer; beforeEach(() => { - session = Ember.ObjectProxy.extend(Ember.Evented, { + session = ObjectProxy.extend(Evented, { content: {} }).create(); authorizer = { @@ -28,7 +30,7 @@ describe('SessionService', () => { sessionService.one('authenticationSucceeded', () => triggered = true); session.trigger('authenticationSucceeded'); - Ember.run.next(() => { + next(() => { expect(triggered).to.be.true; done(); }); @@ -39,7 +41,7 @@ describe('SessionService', () => { sessionService.one('invalidationSucceeded', () => triggered = true); session.trigger('invalidationSucceeded'); - Ember.run.next(() => { + next(() => { expect(triggered).to.be.true; done(); }); @@ -100,6 +102,12 @@ describe('SessionService', () => { expect(session.content).to.eql({ some: { other: 'data' } }); }); + it('can be set with Ember.set', () => { + set(sessionService, 'data.emberSet', 'ember-set-data'); + + expect(session.content).to.eql({ emberSet: 'ember-set-data' }); + }); + it('is read-only', () => { expect(() => { sessionService.set('data', false); diff --git a/tests/unit/session-stores/shared/cookie-store-behavior.js b/tests/unit/session-stores/shared/cookie-store-behavior.js index e38049290..5ade9272b 100644 --- a/tests/unit/session-stores/shared/cookie-store-behavior.js +++ b/tests/unit/session-stores/shared/cookie-store-behavior.js @@ -6,6 +6,8 @@ import { expect } from 'chai'; import sinon from 'sinon'; import FakeCookieService from '../../../helpers/fake-cookie-service'; +const { run: { next } } = Ember; + export default function(options) { let store; let createStore; @@ -34,14 +36,22 @@ export default function(options) { store = createStore(cookieService, { cookieName: 'test:session' }); store.persist({ key: 'value' }); - expect(cookieService.write).to.have.been.calledWith('test:session', JSON.stringify({ key: 'value' }), { domain: null, expires: null, path: '/', secure: false }); + expect(cookieService.write).to.have.been.calledWith( + 'test:session', + JSON.stringify({ key: 'value' }), + { domain: null, expires: null, path: '/', secure: false } + ); }); it('respects the configured cookieDomain', () => { store = createStore(cookieService, { cookieDomain: 'example.com' }); store.persist({ key: 'value' }); - expect(cookieService.write).to.have.been.calledWith('ember_simple_auth:session', JSON.stringify({ key: 'value' }), { domain: 'example.com', expires: null, path: '/', secure: false }); + expect(cookieService.write).to.have.been.calledWith( + 'ember_simple_auth-session', + JSON.stringify({ key: 'value' }), + { domain: 'example.com', expires: null, path: '/', secure: false } + ); }); }); @@ -72,10 +82,10 @@ export default function(options) { }); it('is not triggered when the cookie has not actually changed', (done) => { - document.cookie = 'ember_simple_auth:session=%7B%22key%22%3A%22value%22%7D;path=/;'; + document.cookie = 'ember_simple_auth-session=%7B%22key%22%3A%22value%22%7D;path=/;'; sync(store); - Ember.run.next(() => { + next(() => { expect(triggered).to.be.false; done(); }); @@ -83,11 +93,11 @@ export default function(options) { it('is triggered when the cookie changed', (done) => { const cookiesService = store.get('_cookies') || store.get('_store._cookies'); - cookiesService._content['ember_simple_auth:session'] = '%7B%22key%22%3A%22other%20value%22%7D'; + cookiesService._content['ember_simple_auth-session'] = '%7B%22key%22%3A%22other%20value%22%7D'; sync(store); - Ember.run.next(() => { - Ember.run.next(() => { + next(() => { + next(() => { expect(triggered).to.be.true; done(); }); @@ -98,7 +108,7 @@ export default function(options) { renew(store, { key: 'value' }); sync(store); - Ember.run.next(() => { + next(() => { expect(triggered).to.be.false; done(); }); diff --git a/tests/unit/session-stores/shared/store-behavior.js b/tests/unit/session-stores/shared/store-behavior.js index 551e37c9a..c83c16ee9 100644 --- a/tests/unit/session-stores/shared/store-behavior.js +++ b/tests/unit/session-stores/shared/store-behavior.js @@ -4,8 +4,10 @@ import { it } from 'ember-mocha'; import { describe, beforeEach } from 'mocha'; import { expect } from 'chai'; +const { run: { next }, K } = Ember; + export default function(options) { - let syncExternalChanges = options.syncExternalChanges || Ember.K; + let syncExternalChanges = options.syncExternalChanges || K; let store; beforeEach(() => { @@ -37,7 +39,7 @@ export default function(options) { store.persist({ key: 'other value' }); syncExternalChanges(); - Ember.run.next(() => { + next(() => { expect(triggered).to.be.false; done(); }); diff --git a/vendor/ember-simple-auth/register-version.js b/vendor/ember-simple-auth/register-version.js index 24dd74ed8..a75f8ffa0 100644 --- a/vendor/ember-simple-auth/register-version.js +++ b/vendor/ember-simple-auth/register-version.js @@ -1 +1 @@ -Ember.libraries.register('Ember Simple Auth', '1.1.0-beta.4'); +Ember.libraries.register('Ember Simple Auth', '1.1.0');