From 3e5a62309fd73219837c57691313906285e4cde3 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Sat, 6 Oct 2018 01:46:33 +0700 Subject: [PATCH] Use Admin API v2 with session auth (#1046) refs #9865 - removed all `oauth2` and token-based ESA auth - added new `cookie` authenticator which handles session creation - updated the session store to extend from the `ephemeral` in-memory store and to restore by fetching the currently logged in user and using the success/failure state to indicate authentication state - ESA automatically calls this `.restore()` method on app boot - the `session` service caches the current-user query so there's no unnecessary requests being made for the "logged in" state - removed the now-unnecessary token refresh and logout routines from the `application` route - removed the now-unnecessary token refresh routines from the `ajax` service - removed `access_token` query param from iframe file downloaders - changed Ember Data adapters and `ghost-paths` to use the `/ghost/api/v2/admin/` namespace --- ghost/admin/app/adapters/base.js | 9 +- ghost/admin/app/authenticators/cookie.js | 41 ++++++++ ghost/admin/app/authenticators/oauth2.js | 91 ----------------- .../app/components/modal-re-authenticate.js | 2 +- ghost/admin/app/controllers/reset.js | 2 +- .../admin/app/controllers/settings/design.js | 4 +- ghost/admin/app/controllers/settings/labs.js | 4 +- ghost/admin/app/controllers/setup/two.js | 2 +- ghost/admin/app/controllers/signin.js | 2 +- ghost/admin/app/controllers/signup.js | 2 +- ghost/admin/app/routes/application.js | 47 --------- ghost/admin/app/routes/signout.js | 14 +-- ghost/admin/app/services/ajax.js | 44 ++------- ghost/admin/app/session-stores/application.js | 32 ++++-- ghost/admin/app/utils/ghost-paths.js | 2 +- ghost/admin/mirage/config.js | 2 +- ghost/admin/mirage/config/authentication.js | 9 +- ghost/admin/package.json | 1 - .../tests/acceptance/authentication-test.js | 74 -------------- .../tests/acceptance/error-handling-test.js | 2 +- ghost/admin/tests/acceptance/setup-test.js | 2 +- ghost/admin/tests/acceptance/signin-test.js | 22 +---- .../tests/integration/adapters/tag-test.js | 4 +- .../tests/integration/adapters/user-test.js | 6 +- .../components/gh-file-uploader-test.js | 12 +-- .../components/gh-image-uploader-test.js | 25 +---- .../components/gh-uploader-test.js | 12 +-- .../tests/integration/services/ajax-test.js | 99 ++----------------- .../tests/integration/services/config-test.js | 4 +- .../integration/services/feature-test.js | 8 +- .../services/slug-generator-test.js | 2 +- .../tests/integration/services/store-test.js | 2 +- .../tests/unit/authenticators/cookie-test.js | 71 +++++++++++++ ghost/admin/tests/unit/models/invite-test.js | 2 +- ghost/admin/yarn.lock | 5 - 35 files changed, 210 insertions(+), 452 deletions(-) create mode 100644 ghost/admin/app/authenticators/cookie.js delete mode 100644 ghost/admin/app/authenticators/oauth2.js create mode 100644 ghost/admin/tests/unit/authenticators/cookie-test.js diff --git a/ghost/admin/app/adapters/base.js b/ghost/admin/app/adapters/base.js index 08e81644ce..2a95ca8050 100644 --- a/ghost/admin/app/adapters/base.js +++ b/ghost/admin/app/adapters/base.js @@ -14,14 +14,9 @@ export default RESTAdapter.extend(DataAdapterMixin, AjaxServiceSupport, { return false; }, - /* eslint-disable camelcase */ - authorize(xhr) { - if (this.get('session.isAuthenticated')) { - let {access_token} = this.get('session.data.authenticated'); - xhr.setRequestHeader('Authorization', `Bearer ${access_token}`); - } + authorize(/*xhr*/) { + // noop - we're using server-side session cookies }, - /* eslint-enable camelcase */ query(store, type, query) { let id; diff --git a/ghost/admin/app/authenticators/cookie.js b/ghost/admin/app/authenticators/cookie.js new file mode 100644 index 0000000000..32b9370809 --- /dev/null +++ b/ghost/admin/app/authenticators/cookie.js @@ -0,0 +1,41 @@ +import Authenticator from 'ember-simple-auth/authenticators/base'; +import RSVP from 'rsvp'; +import {computed} from '@ember/object'; +import {inject as service} from '@ember/service'; + +export default Authenticator.extend({ + ajax: service(), + ghostPaths: service(), + + sessionEndpoint: computed('ghostPaths.apiRoot', function () { + return `${this.ghostPaths.apiRoot}/session`; + }), + + restore: function () { + return RSVP.resolve(); + }, + + authenticate(identification, password) { + const data = {username: identification, password}; + const options = { + data, + contentType: 'application/json;charset=utf-8', + // ember-ajax will try and parse the response as JSON if not explicitly set + dataType: 'text' + }; + + return this.ajax.post(this.sessionEndpoint, options); + }, + + invalidate() { + // if we're invalidating because of a 401 we can end up in an infinite + // loop if we then try to perform a DELETE /session/ request + // TODO: find a more elegant way to handle this + if (this.ajax.skipSessionDeletion) { + this.ajax.skipSessionDeletion = false; + return RSVP.resolve(); + } + + return this.ajax.del(this.sessionEndpoint); + } +}); diff --git a/ghost/admin/app/authenticators/oauth2.js b/ghost/admin/app/authenticators/oauth2.js deleted file mode 100644 index a0c8cbce60..0000000000 --- a/ghost/admin/app/authenticators/oauth2.js +++ /dev/null @@ -1,91 +0,0 @@ -import Authenticator from 'ember-simple-auth/authenticators/oauth2-password-grant'; -import RSVP from 'rsvp'; -import {assign} from '@ember/polyfills'; -import {computed} from '@ember/object'; -import {isEmpty} from '@ember/utils'; -import {run} from '@ember/runloop'; -import {inject as service} from '@ember/service'; -import {makeArray as wrap} from '@ember/array'; - -export default Authenticator.extend({ - ajax: service(), - session: service(), - config: service(), - ghostPaths: service(), - - init() { - this._super(...arguments); - - let handler = run.bind(this, () => { - this.onOnline(); - }); - window.addEventListener('online', handler); - }, - - serverTokenEndpoint: computed('ghostPaths.apiRoot', function () { - return `${this.get('ghostPaths.apiRoot')}/authentication/token`; - }), - - // disable general token revocation because the requests will always 401 - // (revocation is triggered by invalid access token so it's already invalid) - // we have a separate logout procedure that sends revocation requests - serverTokenRevocationEndpoint: null, - - makeRequest(url, data) { - /* eslint-disable camelcase */ - data.client_id = this.get('config.clientId'); - data.client_secret = this.get('config.clientSecret'); - /* eslint-enable camelcase */ - - let options = { - data, - dataType: 'json', - contentType: 'application/x-www-form-urlencoded' - }; - - return this.get('ajax').post(url, options); - }, - - /** - * Invoked when "navigator.online" event is trigerred. - * This is a helper function to handle intermittent internet connectivity. Token is refreshed - * when browser status becomes "online". - */ - onOnline() { - if (this.get('session.isAuthenticated')) { - let autoRefresh = this.get('refreshAccessTokens'); - if (autoRefresh) { - let expiresIn = this.get('session.data.authenticated.expires_in'); - let token = this.get('session.data.authenticated.refresh_token'); - return this._refreshAccessToken(expiresIn, token); - } - } - }, - - authenticate(identification, password, scope = [], headers = {}) { - return new RSVP.Promise((resolve, reject) => { - let data = {grant_type: 'password', username: identification, password}; - let serverTokenEndpoint = this.get('serverTokenEndpoint'); - let scopesString = wrap(scope).join(' '); - if (!isEmpty(scopesString)) { - data.scope = scopesString; - } - this.makeRequest(serverTokenEndpoint, data, headers).then((response) => { - run(() => { - /* eslint-disable camelcase */ - let expiresAt = this._absolutizeExpirationTime(response.expires_in); - this._scheduleAccessTokenRefresh(response.expires_in, expiresAt, response.refresh_token); - /* eslint-enable camelcase */ - - if (!isEmpty(expiresAt)) { - response = assign(response, {expires_at: expiresAt}); - } - - resolve(response); - }); - }, (error) => { - reject(error); - }); - }); - } -}); diff --git a/ghost/admin/app/components/modal-re-authenticate.js b/ghost/admin/app/components/modal-re-authenticate.js index 74640b12df..fb34ae0b05 100644 --- a/ghost/admin/app/components/modal-re-authenticate.js +++ b/ghost/admin/app/components/modal-re-authenticate.js @@ -28,7 +28,7 @@ export default ModalComponent.extend(ValidationEngine, { _authenticate() { let session = this.get('session'); - let authStrategy = 'authenticator:oauth2'; + let authStrategy = 'authenticator:cookie'; let identification = this.get('identification'); let password = this.get('password'); diff --git a/ghost/admin/app/controllers/reset.js b/ghost/admin/app/controllers/reset.js index 64ed203ca9..543dd654f8 100644 --- a/ghost/admin/app/controllers/reset.js +++ b/ghost/admin/app/controllers/reset.js @@ -56,7 +56,7 @@ export default Controller.extend(ValidationEngine, { } }); this.get('notifications').showAlert(resp.passwordreset[0].message, {type: 'warn', delayed: true, key: 'password.reset'}); - this.get('session').authenticate('authenticator:oauth2', this.get('email'), credentials.newPassword); + this.get('session').authenticate('authenticator:cookie', this.get('email'), credentials.newPassword); return true; } catch (error) { this.get('notifications').showAPIError(error, {key: 'password.reset'}); diff --git a/ghost/admin/app/controllers/settings/design.js b/ghost/admin/app/controllers/settings/design.js index 4bccf712c9..d4351cdaca 100644 --- a/ghost/admin/app/controllers/settings/design.js +++ b/ghost/admin/app/controllers/settings/design.js @@ -171,9 +171,7 @@ export default Controller.extend({ }, downloadTheme(theme) { - let themeURL = `${this.get('ghostPaths.apiRoot')}/themes/${theme.name}`; - let accessToken = this.get('session.data.authenticated.access_token'); - let downloadURL = `${themeURL}/download/?access_token=${accessToken}`; + let downloadURL = `${this.get('ghostPaths.apiRoot')}/themes/${theme.name}`; let iframe = $('#iframeDownload'); if (iframe.length === 0) { diff --git a/ghost/admin/app/controllers/settings/labs.js b/ghost/admin/app/controllers/settings/labs.js index 7d6a0c9fd0..a3cca27813 100644 --- a/ghost/admin/app/controllers/settings/labs.js +++ b/ghost/admin/app/controllers/settings/labs.js @@ -129,9 +129,7 @@ export default Controller.extend({ }, downloadFile(endpoint) { - let url = this.get('ghostPaths.url').api(endpoint); - let accessToken = this.get('session.data.authenticated.access_token'); - let downloadURL = `${url}?access_token=${accessToken}`; + let downloadURL = this.get('ghostPaths.url').api(endpoint); let iframe = $('#iframeDownload'); if (iframe.length === 0) { diff --git a/ghost/admin/app/controllers/setup/two.js b/ghost/admin/app/controllers/setup/two.js index 79e4be0e3b..a14c54d870 100644 --- a/ghost/admin/app/controllers/setup/two.js +++ b/ghost/admin/app/controllers/setup/two.js @@ -140,7 +140,7 @@ export default Controller.extend(ValidationEngine, { // Don't call the success handler, otherwise we will be redirected to admin this.set('session.skipAuthSuccessHandler', true); - return this.get('session').authenticate('authenticator:oauth2', this.get('email'), this.get('password')).then(() => { + return this.get('session').authenticate('authenticator:cookie', this.get('email'), this.get('password')).then(() => { this.set('blogCreated', true); return this._afterAuthentication(result); }).catch((error) => { diff --git a/ghost/admin/app/controllers/signin.js b/ghost/admin/app/controllers/signin.js index c681074588..dea524fa87 100644 --- a/ghost/admin/app/controllers/signin.js +++ b/ghost/admin/app/controllers/signin.js @@ -79,7 +79,7 @@ export default Controller.extend(ValidationEngine, { validateAndAuthenticate: task(function* () { let signin = this.get('signin'); - let authStrategy = 'authenticator:oauth2'; + let authStrategy = 'authenticator:cookie'; this.set('flowErrors', ''); // Manually trigger events for input fields, ensuring legacy compatibility with diff --git a/ghost/admin/app/controllers/signup.js b/ghost/admin/app/controllers/signup.js index 5cc1e767db..d645bdbd41 100644 --- a/ghost/admin/app/controllers/signup.js +++ b/ghost/admin/app/controllers/signup.js @@ -133,7 +133,7 @@ export default Controller.extend({ let password = this.get('signupDetails.password'); return this.get('session') - .authenticate('authenticator:oauth2', email, password); + .authenticate('authenticator:cookie', email, password); }, _sendImage: task(function* () { diff --git a/ghost/admin/app/routes/application.js b/ghost/admin/app/routes/application.js index 79d772f907..deb99050c1 100644 --- a/ghost/admin/app/routes/application.js +++ b/ghost/admin/app/routes/application.js @@ -4,7 +4,6 @@ import RSVP from 'rsvp'; import Route from '@ember/routing/route'; import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route'; import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd'; -import moment from 'moment'; import windowProxy from 'ghost-admin/utils/window-proxy'; import {htmlSafe} from '@ember/string'; import { @@ -56,23 +55,6 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, { transition.send('loadServerNotifications'); transition.send('checkForOutdatedDesktopApp'); - // trigger a background token refresh to enable "infinite" sessions - // NOTE: we only do this if the last refresh was > 1 day ago to avoid - // potential issues with multiple tabs and concurrent admin loads/refreshes. - // see https://github.com/TryGhost/Ghost/issues/8616 - let session = this.get('session.session'); - let expiresIn = session.get('authenticated.expires_in') * 1000; - let expiresAt = session.get('authenticated.expires_at'); - let lastRefresh = moment(expiresAt - expiresIn); - let oneDayAgo = moment().subtract(1, 'day'); - - if (lastRefresh.isBefore(oneDayAgo)) { - let authenticator = session._lookupAuthenticator(session.authenticator); - if (authenticator && authenticator.onOnline) { - authenticator.onOnline(); - } - } - let featurePromise = this.get('feature').fetch(); let settingsPromise = this.get('settings').fetch(); let privateConfigPromise = this.get('config').fetchPrivate(); @@ -109,35 +91,6 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, { this.send('loadServerNotifications', true); }, - // this is only called by the `signout` route at present. - // it's separate to the normal ESA session invalidadition because it will - // actually send the token revocation requests whereas we have to avoid - // those most of the time because they will fail if we have invalid tokens - logout() { - let session = this.get('session'); - // revoke keys on the server - if (session.get('isAuthenticated')) { - let auth = session.get('data.authenticated'); - let revokeEndpoint = `${this.get('ghostPaths.apiRoot')}/authentication/revoke`; - let authenticator = session.get('session')._lookupAuthenticator(session.get('session.authenticator')); - let requests = []; - ['refresh_token', 'access_token'].forEach((tokenType) => { - let data = { - token_type_hint: tokenType, - token: auth[tokenType] - }; - authenticator.makeRequest(revokeEndpoint, data); - }); - RSVP.all(requests).finally(() => { - // remove local keys and refresh - session.invalidate(); - }); - } else { - // remove local keys and refresh - session.invalidate(); - } - }, - authorizationFailed() { windowProxy.replaceLocation(AuthConfiguration.baseURL); }, diff --git a/ghost/admin/app/routes/signout.js b/ghost/admin/app/routes/signout.js index 50d5fece73..c58801dd7d 100644 --- a/ghost/admin/app/routes/signout.js +++ b/ghost/admin/app/routes/signout.js @@ -1,11 +1,7 @@ import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; -import Ember from 'ember'; import styleBody from 'ghost-admin/mixins/style-body'; import {inject as service} from '@ember/service'; -// ember-cli-shims doesn't export canInvoke -const {canInvoke} = Ember; - export default AuthenticatedRoute.extend(styleBody, { notifications: service(), @@ -13,12 +9,8 @@ export default AuthenticatedRoute.extend(styleBody, { classNames: ['ghost-signout'], - afterModel(model, transition) { - this.get('notifications').clearAll(); - if (canInvoke(transition, 'send')) { - transition.send('logout'); - } else { - this.send('logout'); - } + afterModel(/*model, transition*/) { + this.notifications.clearAll(); + this.session.invalidate(); } }); diff --git a/ghost/admin/app/services/ajax.js b/ghost/admin/app/services/ajax.js index eff9ed63f7..148d356f92 100644 --- a/ghost/admin/app/services/ajax.js +++ b/ghost/admin/app/services/ajax.js @@ -9,7 +9,6 @@ import {inject as service} from '@ember/service'; const JSON_CONTENT_TYPE = 'application/json'; const GHOST_REQUEST = /\/ghost\/api\//; -const TOKEN_REQUEST = /authentication\/(?:token|ghost|revoke)/; function isJSONContentType(header) { if (!header || isNone(header)) { @@ -119,57 +118,33 @@ export function isThemeValidationError(errorOrStatus, payload) { let ajaxService = AjaxService.extend({ session: service(), + // flag to tell our ESA authenticator not to try an invalidate DELETE request + // because it's been triggered by this service's 401 handling which means the + // DELETE would fail and get stuck in an infinite loop + // TODO: find a more elegant way to handle this + skipSessionDeletion: false, + headers: computed('session.isAuthenticated', function () { - let session = this.get('session'); let headers = {}; headers['X-Ghost-Version'] = config.APP.version; headers['App-Pragma'] = 'no-cache'; - if (session.get('isAuthenticated')) { - /* eslint-disable camelcase */ - let {access_token} = session.get('data.authenticated'); - headers.Authorization = `Bearer ${access_token}`; - /* eslint-enable camelcase */ - } - return headers; }).volatile(), // ember-ajax recognises `application/vnd.api+json` as a JSON-API request // and formats appropriately, we want to handle `application/json` the same _makeRequest(hash) { - let isAuthenticated = this.get('session.isAuthenticated'); - let isGhostRequest = GHOST_REQUEST.test(hash.url); - let isTokenRequest = isGhostRequest && TOKEN_REQUEST.test(hash.url); - let tokenExpiry = this.get('session.authenticated.expires_at'); - let isTokenExpired = tokenExpiry < (new Date()).getTime(); - if (isJSONContentType(hash.contentType) && hash.type !== 'GET') { if (typeof hash.data === 'object') { hash.data = JSON.stringify(hash.data); } } - // we can get into a situation where the app is left open without a - // network connection and the token subsequently expires, this will - // result in the next network request returning a 401 and killing the - // session. This is an attempt to detect that and restore the session - // using the stored refresh token before continuing with the request - // - // TODO: - // - this might be quite blunt, if we have a lot of requests at once - // we probably want to queue the requests until the restore completes - // BUG: - // - the original caller gets a rejected promise with `undefined` instead - // of the AjaxError object when session restore fails. This isn't a - // huge deal because the session will be invalidated and app reloaded - // but it would be nice to be consistent - if (isAuthenticated && isGhostRequest && !isTokenRequest && isTokenExpired) { - return this.get('session').restore().then(() => this._makeRequest(hash)); - } + hash.withCredentials = true; - return this._super(...arguments); + return this._super(hash); }, handleResponse(status, headers, payload, request) { @@ -192,7 +167,8 @@ let ajaxService = AjaxService.extend({ let isUnauthorized = this.isUnauthorizedError(status, headers, payload); if (isAuthenticated && isGhostRequest && isUnauthorized) { - this.get('session').invalidate(); + this.skipSessionDeletion = true; + this.session.invalidate(); } return this._super(...arguments); diff --git a/ghost/admin/app/session-stores/application.js b/ghost/admin/app/session-stores/application.js index fe8296e4a9..cbad2ed3e5 100644 --- a/ghost/admin/app/session-stores/application.js +++ b/ghost/admin/app/session-stores/application.js @@ -1,10 +1,28 @@ -import AdaptiveStore from 'ember-simple-auth/session-stores/adaptive'; -import ghostPaths from 'ghost-admin/utils/ghost-paths'; +import EphemeralStore from 'ember-simple-auth/session-stores/ephemeral'; +import RSVP from 'rsvp'; +import {inject as service} from '@ember/service'; -const paths = ghostPaths(); -const keyName = `ghost${(paths.subdir.indexOf('/') === 0 ? `-${paths.subdir.substr(1)}` : '') }:session`; +// Ghost already uses a cookie to store it's session so we don't need to keep +// track of any other peristent login state separately in Ember Simple Auth +export default EphemeralStore.extend({ + session: service(), -export default AdaptiveStore.extend({ - localStorageKey: keyName, - cookieName: keyName + // when loading the app we want ESA to try fetching the currently logged + // in user. This will succeed/fail depending on whether we have a valid + // session cookie or not so we can use that as an indication of the session + // being authenticated + restore() { + return this.session.user.then(() => { + // provide the necessary data for internal-session to mark the + // session as authenticated + let data = {authenticated: {authenticator: 'authenticator:cookie'}}; + this.persist(data); + return data; + }).catch(() => { + // ensure the session.user doesn't return the same rejected promise + // after a succussful login + this.session.notifyPropertyChange('user'); + return RSVP.reject(); + }); + } }); diff --git a/ghost/admin/app/utils/ghost-paths.js b/ghost/admin/app/utils/ghost-paths.js index 9796422def..cf37a409f9 100644 --- a/ghost/admin/app/utils/ghost-paths.js +++ b/ghost/admin/app/utils/ghost-paths.js @@ -18,7 +18,7 @@ export default function () { let subdir = path.substr(0, path.search('/ghost/')); let adminRoot = `${subdir}/ghost/`; let assetRoot = `${subdir}/ghost/assets/`; - let apiRoot = `${subdir}/ghost/api/v0.1`; + let apiRoot = `${subdir}/ghost/api/v2/admin`; function assetUrl(src) { return subdir + src; diff --git a/ghost/admin/mirage/config.js b/ghost/admin/mirage/config.js index 9005a9bbf7..ea10db0e0d 100644 --- a/ghost/admin/mirage/config.js +++ b/ghost/admin/mirage/config.js @@ -35,7 +35,7 @@ export default function () { export function testConfig() { this.passthrough('/write-coverage'); // For code coverage // this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server - this.namespace = '/ghost/api/v0.1'; // make this `api`, for example, if your API is namespaced + this.namespace = '/ghost/api/v2/admin'; // make this `api`, for example, if your API is namespaced // this.timing = 400; // delay for each request, automatically set to 0 during testing // this.logging = true; diff --git a/ghost/admin/mirage/config/authentication.js b/ghost/admin/mirage/config/authentication.js index dba9607f2a..d29ef70fd9 100644 --- a/ghost/admin/mirage/config/authentication.js +++ b/ghost/admin/mirage/config/authentication.js @@ -3,14 +3,9 @@ import {Response} from 'ember-cli-mirage'; import {isBlank} from '@ember/utils'; export default function mockAuthentication(server) { - server.post('/authentication/token', function () { + server.post('/session', function () { // Password sign-in - return { - access_token: 'MirageAccessToken', - expires_in: 172800, - refresh_token: 'MirageRefreshToken', - token_type: 'Bearer' - }; + return new Response(201); }); server.post('/authentication/passwordreset', function (schema, request) { diff --git a/ghost/admin/package.json b/ghost/admin/package.json index ac4ce965c1..a3a0893e43 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -41,7 +41,6 @@ "coveralls": "3.0.2", "csscomb": "4.2.0", "current-device": "0.7.8", - "deparam": "1.0.5", "element-resize-detector": "^1.1.14", "ember-ajax": "3.1.1", "ember-assign-helper": "0.1.2", diff --git a/ghost/admin/tests/acceptance/authentication-test.js b/ghost/admin/tests/acceptance/authentication-test.js index eec97ee12a..84b7efe82b 100644 --- a/ghost/admin/tests/acceptance/authentication-test.js +++ b/ghost/admin/tests/acceptance/authentication-test.js @@ -1,5 +1,3 @@ -import OAuth2Authenticator from 'ghost-admin/authenticators/oauth2'; -import deparam from 'npm:deparam'; import destroyApp from '../helpers/destroy-app'; import startApp from '../helpers/start-app'; import windowProxy from 'ghost-admin/utils/window-proxy'; @@ -25,88 +23,16 @@ describe('Acceptance: Authentication', function () { beforeEach(function () { // ensure the /users/me route doesn't error server.create('user'); - server.get('authentication/setup', function () { return {setup: [{status: false}]}; }); }); - it('redirects to setup when setup isn\'t complete', async function () { await visit('settings/labs'); - expect(currentURL()).to.equal('/setup/one'); }); }); - describe('token handling', function () { - beforeEach(function () { - // replace the default test authenticator with our own authenticator - application.register('authenticator:test', OAuth2Authenticator); - - let role = server.create('role', {name: 'Administrator'}); - server.create('user', {roles: [role], slug: 'test-user'}); - }); - - it('refreshes tokens on boot if last refreshed > 24hrs ago', async function () { - /* eslint-disable camelcase */ - // the tokens here don't matter, we're using the actual oauth - // authenticator so we get the tokens back from the mirage endpoint - await authenticateSession(application, { - access_token: 'access_token', - refresh_token: 'refresh_token' - }); - - // authenticating the session above will trigger a token refresh - // request so we need to clear it to ensure we aren't testing the - // test behaviour instead of application behaviour - server.pretender.handledRequests = []; - - // fake a longer session so it appears that we last refreshed > 24hrs ago - let {__container__: container} = application; - let {session} = container.lookup('service:session'); - let newSession = session.get('content'); - newSession.authenticated.expires_in = 172800 * 2; - session.get('store').persist(newSession); - /* eslint-enable camelcase */ - - await visit('/'); - - let requests = server.pretender.handledRequests; - let refreshRequest = requests.findBy('url', '/ghost/api/v0.1/authentication/token'); - - expect(refreshRequest, 'token refresh request').to.exist; - expect(refreshRequest.method, 'method').to.equal('POST'); - - let requestBody = deparam(refreshRequest.requestBody); - expect(requestBody.grant_type, 'grant_type').to.equal('refresh_token'); - expect(requestBody.refresh_token, 'refresh_token').to.equal('MirageRefreshToken'); - }); - - it('doesn\'t refresh tokens on boot if last refreshed < 24hrs ago', async function () { - /* eslint-disable camelcase */ - // the tokens here don't matter, we're using the actual oauth - // authenticator so we get the tokens back from the mirage endpoint - await authenticateSession(application, { - access_token: 'access_token', - refresh_token: 'refresh_token' - }); - /* eslint-enable camelcase */ - - // authenticating the session above will trigger a token refresh - // request so we need to clear it to ensure we aren't testing the - // test behaviour instead of application behaviour - server.pretender.handledRequests = []; - - // we've only just refreshed tokens above so we should always be < 24hrs - await visit('/'); - - let requests = server.pretender.handledRequests; - let refreshRequest = requests.findBy('url', '/ghost/api/v0.1/authentication/token'); - - expect(refreshRequest, 'refresh request').to.not.exist; - }); - }); - describe('general page', function () { let newLocation; diff --git a/ghost/admin/tests/acceptance/error-handling-test.js b/ghost/admin/tests/acceptance/error-handling-test.js index c401239478..d3339f79a1 100644 --- a/ghost/admin/tests/acceptance/error-handling-test.js +++ b/ghost/admin/tests/acceptance/error-handling-test.js @@ -100,7 +100,7 @@ describe('Acceptance: Error Handling', function () { describe('logged out', function () { it('displays alert', async function () { - server.post('/authentication/token', versionMismatchResponse); + server.post('/session', versionMismatchResponse); await visit('/signin'); await fillIn('[name="identification"]', 'test@example.com'); diff --git a/ghost/admin/tests/acceptance/setup-test.js b/ghost/admin/tests/acceptance/setup-test.js index ed909e9dbe..dd25414b37 100644 --- a/ghost/admin/tests/acceptance/setup-test.js +++ b/ghost/admin/tests/acceptance/setup-test.js @@ -200,7 +200,7 @@ describe('Acceptance: Setup', function () { it('handles invalid origin error on step 2', async function () { // mimick the API response for an invalid origin - server.post('/authentication/token', function () { + server.post('/session', function () { return new Response(401, {}, { errors: [ { diff --git a/ghost/admin/tests/acceptance/signin-test.js b/ghost/admin/tests/acceptance/signin-test.js index 9810484602..53c90bdc1c 100644 --- a/ghost/admin/tests/acceptance/signin-test.js +++ b/ghost/admin/tests/acceptance/signin-test.js @@ -1,4 +1,3 @@ -import deparam from 'npm:deparam'; import destroyApp from '../helpers/destroy-app'; import startApp from '../helpers/start-app'; import {Response} from 'ember-cli-mirage'; @@ -37,26 +36,16 @@ describe('Acceptance: Signin', function () { let role = server.create('role', {name: 'Administrator'}); server.create('user', {roles: [role], slug: 'test-user'}); - server.post('/authentication/token', function (schema, {requestBody}) { - /* eslint-disable camelcase */ + server.post('/session', function (schema, {requestBody}) { let { - grant_type: grantType, username, - password, - client_id: clientId - } = deparam(requestBody); + password + } = JSON.parse(requestBody); - expect(grantType, 'grant type').to.equal('password'); - expect(username, 'username').to.equal('test@example.com'); - expect(clientId, 'client id').to.equal('ghost-admin'); + expect(username).to.equal('test@example.com'); if (password === 'thisissupersafe') { - return { - access_token: 'MirageAccessToken', - expires_in: 3600, - refresh_token: 'MirageRefreshToken', - token_type: 'Bearer' - }; + return new Response(201); } else { return new Response(401, {}, { errors: [{ @@ -65,7 +54,6 @@ describe('Acceptance: Signin', function () { }] }); } - /* eslint-enable camelcase */ }); }); diff --git a/ghost/admin/tests/integration/adapters/tag-test.js b/ghost/admin/tests/integration/adapters/tag-test.js index 90be1feaab..7d18523e2f 100644 --- a/ghost/admin/tests/integration/adapters/tag-test.js +++ b/ghost/admin/tests/integration/adapters/tag-test.js @@ -20,7 +20,7 @@ describe('Integration: Adapter: tag', function () { }); it('loads tags from regular endpoint when all are fetched', function (done) { - server.get('/ghost/api/v0.1/tags/', function () { + server.get('/ghost/api/v2/admin/tags/', function () { return [200, {'Content-Type': 'application/json'}, JSON.stringify({tags: [ { id: 1, @@ -42,7 +42,7 @@ describe('Integration: Adapter: tag', function () { }); it('loads tag from slug endpoint when single tag is queried and slug is passed in', function (done) { - server.get('/ghost/api/v0.1/tags/slug/tag-1/', function () { + server.get('/ghost/api/v2/admin/tags/slug/tag-1/', function () { return [200, {'Content-Type': 'application/json'}, JSON.stringify({tags: [ { id: 1, diff --git a/ghost/admin/tests/integration/adapters/user-test.js b/ghost/admin/tests/integration/adapters/user-test.js index b5b98c5295..0faa1590a2 100644 --- a/ghost/admin/tests/integration/adapters/user-test.js +++ b/ghost/admin/tests/integration/adapters/user-test.js @@ -20,7 +20,7 @@ describe('Integration: Adapter: user', function () { }); it('loads users from regular endpoint when all are fetched', function (done) { - server.get('/ghost/api/v0.1/users/', function () { + server.get('/ghost/api/v2/admin/users/', function () { return [200, {'Content-Type': 'application/json'}, JSON.stringify({users: [ { id: 1, @@ -42,7 +42,7 @@ describe('Integration: Adapter: user', function () { }); it('loads user from slug endpoint when single user is queried and slug is passed in', function (done) { - server.get('/ghost/api/v0.1/users/slug/user-1/', function () { + server.get('/ghost/api/v2/admin/users/slug/user-1/', function () { return [200, {'Content-Type': 'application/json'}, JSON.stringify({users: [ { id: 1, @@ -60,7 +60,7 @@ describe('Integration: Adapter: user', function () { }); it('handles "include" parameter when querying single user via slug', function (done) { - server.get('/ghost/api/v0.1/users/slug/user-1/', (request) => { + server.get('/ghost/api/v2/admin/users/slug/user-1/', (request) => { let params = request.queryParams; expect(params.include, 'include query').to.equal('roles,count.posts'); diff --git a/ghost/admin/tests/integration/components/gh-file-uploader-test.js b/ghost/admin/tests/integration/components/gh-file-uploader-test.js index a52af0e241..d52f4cdf22 100644 --- a/ghost/admin/tests/integration/components/gh-file-uploader-test.js +++ b/ghost/admin/tests/integration/components/gh-file-uploader-test.js @@ -18,13 +18,13 @@ const notificationsStub = Service.extend({ }); const stubSuccessfulUpload = function (server, delay = 0) { - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { return [200, {'Content-Type': 'application/json'}, '"/content/images/test.png"']; }, delay); }; const stubFailedUpload = function (server, code, error, delay = 0) { - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { return [code, {'Content-Type': 'application/json'}, JSON.stringify({ errors: [{ errorType: error, @@ -43,7 +43,7 @@ describe('Integration: Component: gh-file-uploader', function () { beforeEach(function () { server = new Pretender(); - this.set('uploadUrl', '/ghost/api/v0.1/uploads/'); + this.set('uploadUrl', '/ghost/api/v2/admin/uploads/'); this.register('service:notifications', notificationsStub); this.inject.service('notifications', {as: 'notifications'}); @@ -90,7 +90,7 @@ describe('Integration: Component: gh-file-uploader', function () { wait().then(() => { expect(server.handledRequests.length).to.equal(1); - expect(server.handledRequests[0].url).to.equal('/ghost/api/v0.1/uploads/'); + expect(server.handledRequests[0].url).to.equal('/ghost/api/v2/admin/uploads/'); done(); }); }); @@ -214,7 +214,7 @@ describe('Integration: Component: gh-file-uploader', function () { }); it('handles file too large error directly from the web server', function (done) { - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { return [413, {}, '']; }); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); @@ -240,7 +240,7 @@ describe('Integration: Component: gh-file-uploader', function () { }); it('handles unknown failure', function (done) { - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { return [500, {'Content-Type': 'application/json'}, '']; }); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); diff --git a/ghost/admin/tests/integration/components/gh-image-uploader-test.js b/ghost/admin/tests/integration/components/gh-image-uploader-test.js index ce5cbe9313..ca44dd038a 100644 --- a/ghost/admin/tests/integration/components/gh-image-uploader-test.js +++ b/ghost/admin/tests/integration/components/gh-image-uploader-test.js @@ -29,13 +29,13 @@ const sessionStub = Service.extend({ }); const stubSuccessfulUpload = function (server, delay = 0) { - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { return [200, {'Content-Type': 'application/json'}, '"/content/images/test.png"']; }, delay); }; const stubFailedUpload = function (server, code, error, delay = 0) { - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { return [code, {'Content-Type': 'application/json'}, JSON.stringify({ errors: [{ errorType: error, @@ -89,27 +89,12 @@ describe('Integration: Component: gh-image-uploader', function () { wait().then(() => { expect(server.handledRequests.length).to.equal(1); - expect(server.handledRequests[0].url).to.equal('/ghost/api/v0.1/uploads/'); + expect(server.handledRequests[0].url).to.equal('/ghost/api/v2/admin/uploads/'); expect(server.handledRequests[0].requestHeaders.Authorization).to.be.undefined; done(); }); }); - it('adds authentication headers to request', function (done) { - stubSuccessfulUpload(server); - - this.get('sessionService').set('isAuthenticated', true); - - this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); - - wait().then(() => { - let [request] = server.handledRequests; - expect(request.requestHeaders.Authorization).to.equal('Bearer AccessMe123'); - done(); - }); - }); - it('fires update action on successful upload', function (done) { let update = sinon.spy(); this.set('update', update); @@ -229,7 +214,7 @@ describe('Integration: Component: gh-image-uploader', function () { }); it('handles file too large error directly from the web server', function (done) { - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { return [413, {}, '']; }); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); @@ -255,7 +240,7 @@ describe('Integration: Component: gh-image-uploader', function () { }); it('handles unknown failure', function (done) { - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { return [500, {'Content-Type': 'application/json'}, '']; }); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); diff --git a/ghost/admin/tests/integration/components/gh-uploader-test.js b/ghost/admin/tests/integration/components/gh-uploader-test.js index d4dfd28861..7625b9b1bc 100644 --- a/ghost/admin/tests/integration/components/gh-uploader-test.js +++ b/ghost/admin/tests/integration/components/gh-uploader-test.js @@ -10,13 +10,13 @@ import {run} from '@ember/runloop'; import {setupComponentTest} from 'ember-mocha'; const stubSuccessfulUpload = function (server, delay = 0) { - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { return [200, {'Content-Type': 'application/json'}, '"/content/images/test.png"']; }, delay); }; const stubFailedUpload = function (server, code, error, delay = 0) { - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { return [code, {'Content-Type': 'application/json'}, JSON.stringify({ errors: [{ errorType: error, @@ -54,7 +54,7 @@ describe('Integration: Component: gh-uploader', function () { let [lastRequest] = server.handledRequests; expect(server.handledRequests.length).to.equal(1); - expect(lastRequest.url).to.equal('/ghost/api/v0.1/uploads/'); + expect(lastRequest.url).to.equal('/ghost/api/v2/admin/uploads/'); // requestBody is a FormData object // this will fail in anything other than Chrome and Firefox // https://developer.mozilla.org/en-US/docs/Web/API/FormData#Browser_compatibility @@ -139,7 +139,7 @@ describe('Integration: Component: gh-uploader', function () { it('onComplete returns results in same order as selected', async function () { // first request has a delay to simulate larger file - server.post('/ghost/api/v0.1/uploads/', function () { + server.post('/ghost/api/v2/admin/uploads/', function () { // second request has no delay to simulate small file stubSuccessfulUpload(server, 0); @@ -268,7 +268,7 @@ describe('Integration: Component: gh-uploader', function () { }); it('uploads to supplied `uploadUrl`', async function () { - server.post('/ghost/api/v0.1/images/', function () { + server.post('/ghost/api/v2/admin/images/', function () { return [200, {'Content-Type': 'application/json'}, '"/content/images/test.png"']; }); @@ -277,7 +277,7 @@ describe('Integration: Component: gh-uploader', function () { await wait(); let [lastRequest] = server.handledRequests; - expect(lastRequest.url).to.equal('/ghost/api/v0.1/images/'); + expect(lastRequest.url).to.equal('/ghost/api/v2/admin/images/'); }); it('passes supplied paramName in request', async function () { diff --git a/ghost/admin/tests/integration/services/ajax-test.js b/ghost/admin/tests/integration/services/ajax-test.js index 0e1443766c..f6dc21859c 100644 --- a/ghost/admin/tests/integration/services/ajax-test.js +++ b/ghost/admin/tests/integration/services/ajax-test.js @@ -1,6 +1,4 @@ import Pretender from 'pretender'; -import RSVP from 'rsvp'; -import Service from '@ember/service'; import config from 'ghost-admin/config/environment'; import {describe, it} from 'mocha'; import {expect} from 'chai'; @@ -9,6 +7,7 @@ import { isUnauthorizedError } from 'ember-ajax/errors'; import { + isMaintenanceError, isRequestEntityTooLargeError, isUnsupportedMediaTypeError, isVersionMismatchError @@ -175,96 +174,16 @@ describe('Integration: Service: ajax', function () { }); }); - /* eslint-disable camelcase */ - describe('session handling', function () { - let sessionStub = Service.extend({ - isAuthenticated: true, - restoreCalled: false, - authenticated: null, + it('handles error checking for MaintenanceError on 503 errors', function (done) { + stubAjaxEndpoint(server, {}, 503); - init() { - this._super(...arguments); - let authenticated = { - expires_at: (new Date()).getTime() - 10000, - access_token: 'AccessMe123', - refresh_token: 'RefreshMe123' - }; - this.authenticated = authenticated; - this.data = {authenticated}; - }, + let ajax = this.subject(); - restore() { - this.restoreCalled = true; - this.authenticated.expires_at = (new Date()).getTime() + 10000; - return RSVP.resolve(); - }, - - authorize() { - - } - }); - - beforeEach(function () { - server.get('/ghost/api/v0.1/test/', function () { - return [ - 200, - {'Content-Type': 'application/json'}, - JSON.stringify({ - success: true - }) - ]; - }); - - server.post('/ghost/api/v0.1/authentication/token', function () { - return [ - 401, - {'Content-Type': 'application/json'}, - JSON.stringify({}) - ]; - }); - }); - - it('can restore an expired session', function (done) { - let ajax = this.subject(); - ajax.set('session', sessionStub.create()); - - ajax.request('/ghost/api/v0.1/test/'); - - ajax.request('/ghost/api/v0.1/test/').then((result) => { - expect(ajax.get('session.restoreCalled'), 'restoreCalled').to.be.true; - expect(result.success, 'result.success').to.be.true; - done(); - }).catch(() => { - expect(true, 'request failed').to.be.false; - done(); - }); - }); - - it('errors correctly when session restoration fails', function (done) { - let ajax = this.subject(); - let invalidateCalled = false; - - ajax.set('session', sessionStub.create()); - ajax.set('session.restore', function () { - this.set('restoreCalled', true); - return ajax.post('/ghost/api/v0.1/authentication/token'); - }); - ajax.set('session.invalidate', function () { - invalidateCalled = true; - }); - - stubAjaxEndpoint(server, {}, 401); - - ajax.request('/ghost/api/v0.1/test/').then(() => { - expect(true, 'request was successful').to.be.false; - done(); - }).catch(() => { - // TODO: fix the error return when a session restore fails - // expect(isUnauthorizedError(error)).to.be.true; - expect(ajax.get('session.restoreCalled'), 'restoreCalled').to.be.true; - expect(invalidateCalled, 'invalidateCalled').to.be.true; - done(); - }); + ajax.request('/test/').then(() => { + expect(false).to.be.true; + }).catch((error) => { + expect(isMaintenanceError(error)).to.be.true; + done(); }); }); }); diff --git a/ghost/admin/tests/integration/services/config-test.js b/ghost/admin/tests/integration/services/config-test.js index 52bd6aaeb3..50a8da8245 100644 --- a/ghost/admin/tests/integration/services/config-test.js +++ b/ghost/admin/tests/integration/services/config-test.js @@ -5,7 +5,7 @@ import {expect} from 'chai'; import {setupTest} from 'ember-mocha'; function stubAvailableTimezonesEndpoint(server) { - server.get('/ghost/api/v0.1/configuration/timezones', function () { + server.get('/ghost/api/v2/admin/configuration/timezones', function () { return [ 200, {'Content-Type': 'application/json'}, @@ -58,7 +58,7 @@ describe('Integration: Service: config', function () { it('normalizes blogUrl to non-trailing-slash', function (done) { let stubBlogUrl = function stubBlogUrl(blogUrl) { - server.get('/ghost/api/v0.1/configuration/', function () { + server.get('/ghost/api/v2/admin/configuration/', function () { return [ 200, {'Content-Type': 'application/json'}, diff --git a/ghost/admin/tests/integration/services/feature-test.js b/ghost/admin/tests/integration/services/feature-test.js index 95ca9a16a3..a8e4a3f654 100644 --- a/ghost/admin/tests/integration/services/feature-test.js +++ b/ghost/admin/tests/integration/services/feature-test.js @@ -16,11 +16,11 @@ function stubSettings(server, labs, validSave = true) { } ]; - server.get('/ghost/api/v0.1/settings/', function () { + server.get('/ghost/api/v2/admin/settings/', function () { return [200, {'Content-Type': 'application/json'}, JSON.stringify({settings})]; }); - server.put('/ghost/api/v0.1/settings/', function (request) { + server.put('/ghost/api/v2/admin/settings/', function (request) { let statusCode = (validSave) ? 200 : 400; let response = (validSave) ? request.requestBody : JSON.stringify({ errors: [{ @@ -46,11 +46,11 @@ function stubUser(server, accessibility, validSave = true) { }] }]; - server.get('/ghost/api/v0.1/users/me/', function () { + server.get('/ghost/api/v2/admin/users/me/', function () { return [200, {'Content-Type': 'application/json'}, JSON.stringify({users})]; }); - server.put('/ghost/api/v0.1/users/1/', function (request) { + server.put('/ghost/api/v2/admin/users/1/', function (request) { let statusCode = (validSave) ? 200 : 400; let response = (validSave) ? request.requestBody : JSON.stringify({ errors: [{ diff --git a/ghost/admin/tests/integration/services/slug-generator-test.js b/ghost/admin/tests/integration/services/slug-generator-test.js index fb32e6a80a..32366a948e 100644 --- a/ghost/admin/tests/integration/services/slug-generator-test.js +++ b/ghost/admin/tests/integration/services/slug-generator-test.js @@ -5,7 +5,7 @@ import {expect} from 'chai'; import {setupTest} from 'ember-mocha'; function stubSlugEndpoint(server, type, slug) { - server.get('/ghost/api/v0.1/slugs/:type/:slug/', function (request) { + server.get('/ghost/api/v2/admin/slugs/:type/:slug/', function (request) { expect(request.params.type).to.equal(type); expect(request.params.slug).to.equal(slug); diff --git a/ghost/admin/tests/integration/services/store-test.js b/ghost/admin/tests/integration/services/store-test.js index e445ee5672..36c127873c 100644 --- a/ghost/admin/tests/integration/services/store-test.js +++ b/ghost/admin/tests/integration/services/store-test.js @@ -23,7 +23,7 @@ describe('Integration: Service: store', function () { let {version} = config.APP; let store = this.subject(); - server.get('/ghost/api/v0.1/posts/1/', function () { + server.get('/ghost/api/v2/admin/posts/1/', function () { return [ 404, {'Content-Type': 'application/json'}, diff --git a/ghost/admin/tests/unit/authenticators/cookie-test.js b/ghost/admin/tests/unit/authenticators/cookie-test.js new file mode 100644 index 0000000000..0404934937 --- /dev/null +++ b/ghost/admin/tests/unit/authenticators/cookie-test.js @@ -0,0 +1,71 @@ +import RSVP from 'rsvp'; +import Service from '@ember/service'; +import sinon from 'sinon'; +import {beforeEach, describe, it} from 'mocha'; +import {expect} from 'chai'; +import {setupTest} from 'ember-mocha'; + +const mockAjax = Service.extend({ + skipSessionDeletion: false, + init() { + this._super(...arguments); + this.post = sinon.stub().resolves(); + this.del = sinon.stub().resolves(); + } +}); + +const mockGhostPaths = Service.extend({ + apiRoot: '/ghost/api/v2/admin' +}); + +describe('Unit: Authenticator: cookie', () => { + setupTest('authenticator:cookie', {}); + + beforeEach(function () { + this.register('service:ajax', mockAjax); + this.inject.service('ajax', {as: 'ajax'}); + + this.register('service:ghost-paths', mockGhostPaths); + this.inject.service('ghost-paths', {as: 'ghostPaths'}); + }); + + describe('#restore', function () { + it('returns a resolving promise', function () { + return this.subject().restore(); + }); + }); + + describe('#authenticate', function () { + it('posts the username and password to the sessionEndpoint and returns the promise', function () { + let authenticator = this.subject(); + let post = authenticator.ajax.post; + + return authenticator.authenticate('AzureDiamond', 'hunter2').then(() => { + expect(post.args[0][0]).to.equal('/ghost/api/v2/admin/session'); + expect(post.args[0][1]).to.deep.include({ + data: { + username: 'AzureDiamond', + password: 'hunter2' + } + }); + expect(post.args[0][1]).to.deep.include({ + dataType: 'text' + }); + expect(post.args[0][1]).to.deep.include({ + contentType: 'application/json;charset=utf-8' + }); + }); + }); + }); + + describe('#invalidate', function () { + it('makes a delete request to the sessionEndpoint', function () { + let authenticator = this.subject(); + let del = authenticator.ajax.del; + + return authenticator.invalidate().then(() => { + expect(del.args[0][0]).to.equal('/ghost/api/v2/admin/session'); + }); + }); + }); +}); diff --git a/ghost/admin/tests/unit/models/invite-test.js b/ghost/admin/tests/unit/models/invite-test.js index a8ca2ebee3..c8b43c0744 100644 --- a/ghost/admin/tests/unit/models/invite-test.js +++ b/ghost/admin/tests/unit/models/invite-test.js @@ -34,7 +34,7 @@ describe('Unit: Model: invite', function () { let model = this.subject(); let role; - server.post('/ghost/api/v0.1/invites/', function () { + server.post('/ghost/api/v2/admin/invites/', function () { return [200, {}, '{}']; }); diff --git a/ghost/admin/yarn.lock b/ghost/admin/yarn.lock index 4d224c7a2d..d2fe241278 100644 --- a/ghost/admin/yarn.lock +++ b/ghost/admin/yarn.lock @@ -3483,11 +3483,6 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -deparam@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/deparam/-/deparam-1.0.5.tgz#74011bbabd26b40f860c3e3cc2cd61bed4eb43f2" - integrity sha1-dAEbur0mtA+GDD48ws1hvtTrQ/I= - depd@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"