mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Fix auth regressions after ESA 1.0 upgrade
refs #6039, closes #6047, closes #6048 - delete old/unused fixtures file - add failing tests for #6047 & #6048 - redirect to sign-in if we get a 401 when making an API request - fix incorrect `this.notifications` call in tag controller - raise `authorizationFailed` action in application route's `sessionInvalidated` hook so that it can be handled by leaf routes (fixes re-auth modal display) - close "saving failed" alert when successfully re-authenticated - adds a "window-proxy" util so that we can override `window.*` operations in tests - fix `gh-selectize` attempting to register event handlers when the component has already been destroyed
This commit is contained in:
parent
2cfc46d561
commit
73ea9f52f0
16 changed files with 273 additions and 71 deletions
|
@ -1,10 +1,15 @@
|
|||
import DS from 'ember-data';
|
||||
import ghostPaths from 'ghost/utils/ghost-paths';
|
||||
import Ember from 'ember';
|
||||
|
||||
const {inject} = Ember;
|
||||
|
||||
export default DS.RESTAdapter.extend({
|
||||
host: window.location.origin,
|
||||
namespace: ghostPaths().apiRoot.slice(1),
|
||||
|
||||
session: inject.service('session'),
|
||||
|
||||
shouldBackgroundReloadRecord: function () {
|
||||
return false;
|
||||
},
|
||||
|
@ -43,5 +48,15 @@ export default DS.RESTAdapter.extend({
|
|||
return response.then(function () {
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
handleResponse: function (status) {
|
||||
if (status === 401) {
|
||||
if (this.get('session.isAuthenticated')) {
|
||||
this.get('session').invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
return this._super(...arguments);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -17,16 +17,18 @@ export default EmberSelectizeComponent.extend({
|
|||
if (!openOnFocus) {
|
||||
Ember.run.next(this, function () {
|
||||
var selectize = this._selectize;
|
||||
selectize.on('dropdown_open', function () {
|
||||
if (Ember.isBlank(selectize.$control_input.val())) {
|
||||
selectize.close();
|
||||
}
|
||||
});
|
||||
selectize.on('type', function (filter) {
|
||||
if (Ember.isBlank(filter)) {
|
||||
selectize.close();
|
||||
}
|
||||
});
|
||||
if (selectize) {
|
||||
selectize.on('dropdown_open', function () {
|
||||
if (Ember.isBlank(selectize.$control_input.val())) {
|
||||
selectize.close();
|
||||
}
|
||||
});
|
||||
selectize.on('type', function (filter) {
|
||||
if (Ember.isBlank(filter)) {
|
||||
selectize.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -24,6 +24,7 @@ export default Ember.Controller.extend(ValidationEngine, {
|
|||
this.get('session').authenticate(authStrategy, this.get('identification'), this.get('password')).then(function () {
|
||||
self.send('closeModal');
|
||||
self.set('password', '');
|
||||
self.get('notifications').closeAlerts('post.save');
|
||||
}).catch(function () {
|
||||
// if authentication fails a rejected promise will be returned.
|
||||
// it needs to be caught so it doesn't generate an exception in the console,
|
||||
|
|
|
@ -9,6 +9,7 @@ export default Ember.Controller.extend({
|
|||
isMobile: alias('tagsController.isMobile'),
|
||||
|
||||
tagsController: inject.controller('settings.tags'),
|
||||
notifications: inject.service(),
|
||||
|
||||
saveTagProperty: function (propKey, newValue) {
|
||||
const tag = this.get('tag'),
|
||||
|
@ -27,10 +28,10 @@ export default Ember.Controller.extend({
|
|||
|
||||
tag.save().then((savedTag) => {
|
||||
// replace 'new' route with 'tag' route
|
||||
this.replaceWith('settings.tags.tag', savedTag);
|
||||
this.replaceRoute('settings.tags.tag', savedTag);
|
||||
}).catch((error) => {
|
||||
if (error) {
|
||||
this.notifications.showAPIError(error, {key: 'tag.save'});
|
||||
this.get('notifications').showAPIError(error, {key: 'tag.save'});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -52,6 +52,23 @@ export default function () {
|
|||
|
||||
this.get('/notifications/', 'notifications');
|
||||
|
||||
/* Posts ---------------------------------------------------------------- */
|
||||
|
||||
this.post('/posts/', function (db, request) {
|
||||
const [attrs] = JSON.parse(request.requestBody).posts;
|
||||
let post;
|
||||
|
||||
if (isBlank(attrs.slug) && !isBlank(attrs.title)) {
|
||||
attrs.slug = attrs.title.dasherize();
|
||||
}
|
||||
|
||||
post = db.posts.insert(attrs);
|
||||
|
||||
return {
|
||||
posts: [post]
|
||||
};
|
||||
});
|
||||
|
||||
/* Settings ------------------------------------------------------------- */
|
||||
|
||||
this.get('/settings/', function (db, request) {
|
||||
|
@ -84,6 +101,16 @@ export default function () {
|
|||
};
|
||||
});
|
||||
|
||||
/* Slugs ---------------------------------------------------------------- */
|
||||
|
||||
this.get('/slugs/post/:slug/', function (db, request) {
|
||||
return {
|
||||
slugs: [
|
||||
{slug: request.params.slug.dasherize}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
/* Tags ----------------------------------------------------------------- */
|
||||
|
||||
this.post('/tags/', function (db, request) {
|
||||
|
@ -119,7 +146,7 @@ export default function () {
|
|||
|
||||
this.put('/tags/:id/', function (db, request) {
|
||||
const id = request.params.id,
|
||||
[attrs] = JSON.parse(request.requestBody).contacts,
|
||||
[attrs] = JSON.parse(request.requestBody).tags,
|
||||
record = db.tags.update(id, attrs);
|
||||
|
||||
return {
|
||||
|
@ -137,6 +164,8 @@ export default function () {
|
|||
users: [db.users.find(1)]
|
||||
};
|
||||
});
|
||||
|
||||
this.get('/users/', 'users');
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
6
core/client/app/mirage/factories/post.js
Normal file
6
core/client/app/mirage/factories/post.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
/* jscs:disable */
|
||||
import Mirage from 'ember-cli-mirage';
|
||||
|
||||
export default Mirage.Factory.extend({
|
||||
// TODO: fill in with actual factory data
|
||||
});
|
|
@ -1,9 +1,11 @@
|
|||
/* global key */
|
||||
|
||||
import Ember from 'ember';
|
||||
import AuthConfiguration from 'ember-simple-auth/configuration';
|
||||
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
|
||||
import ShortcutsRoute from 'ghost/mixins/shortcuts-route';
|
||||
import ctrlOrCmd from 'ghost/utils/ctrl-or-cmd';
|
||||
import windowProxy from 'ghost/utils/window-proxy';
|
||||
|
||||
const shortcuts = {};
|
||||
|
||||
|
@ -42,6 +44,10 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
|
|||
});
|
||||
},
|
||||
|
||||
sessionInvalidated: function () {
|
||||
this.send('authorizationFailed');
|
||||
},
|
||||
|
||||
actions: {
|
||||
openMobileMenu: function () {
|
||||
this.controller.set('showMobileMenu', true);
|
||||
|
@ -75,6 +81,10 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
|
|||
});
|
||||
},
|
||||
|
||||
authorizationFailed: function () {
|
||||
windowProxy.replaceLocation(AuthConfiguration.baseURL);
|
||||
},
|
||||
|
||||
openModal: function (modalName, model, type) {
|
||||
this.get('dropdown').closeDropdowns();
|
||||
key.setScope('modal');
|
||||
|
|
|
@ -36,7 +36,7 @@ export default AuthenticatedRoute.extend(base, {
|
|||
return post;
|
||||
}
|
||||
|
||||
return self.replaceWith('posts.index');
|
||||
return self.replaceRoute('posts.index');
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -45,7 +45,7 @@ export default AuthenticatedRoute.extend(base, {
|
|||
|
||||
return self.get('session.user').then(function (user) {
|
||||
if (user.get('isAuthor') && !post.isAuthoredByUser(user)) {
|
||||
return self.replaceWith('posts.index');
|
||||
return self.replaceRoute('posts.index');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -32,7 +32,7 @@ export default AuthenticatedRoute.extend(ShortcutsRoute, {
|
|||
return post;
|
||||
}
|
||||
|
||||
return self.replaceWith('posts.index');
|
||||
return self.replaceRoute('posts.index');
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -41,7 +41,7 @@ export default AuthenticatedRoute.extend(ShortcutsRoute, {
|
|||
|
||||
return self.get('session.user').then(function (user) {
|
||||
if (user.get('isAuthor') && !post.isAuthoredByUser(user)) {
|
||||
return self.replaceWith('posts.index');
|
||||
return self.replaceRoute('posts.index');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
{{#gh-editor editorScrollInfo=editorScrollInfo as |ghEditor|}}
|
||||
<header class="view-header">
|
||||
{{#gh-view-title classNames="gh-editor-title" openMobileMenu="openMobileMenu"}}
|
||||
{{gh-trim-focus-input type="text" id="entry-title"placeholder="Your Post Title" value=model.titleScratch
|
||||
tabindex="1" focus=shouldFocusTitle}}
|
||||
{{gh-trim-focus-input type="text" id="entry-title" placeholder="Your Post Title" value=model.titleScratch tabindex="1" focus=shouldFocusTitle}}
|
||||
{{/gh-view-title}}
|
||||
<section class="view-actions">
|
||||
<button type="button" class="post-settings" title="Post Settings" {{action "openSettingsMenu"}}>
|
||||
|
|
9
core/client/app/utils/window-proxy.js
Normal file
9
core/client/app/utils/window-proxy.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export default {
|
||||
changeLocation: function (url) {
|
||||
window.location = url;
|
||||
},
|
||||
|
||||
replaceLocation: function (url) {
|
||||
window.location.replace(url);
|
||||
}
|
||||
};
|
132
core/client/tests/acceptance/authentication-test.js
Normal file
132
core/client/tests/acceptance/authentication-test.js
Normal file
|
@ -0,0 +1,132 @@
|
|||
/* jshint expr:true */
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
beforeEach,
|
||||
afterEach
|
||||
} from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import Ember from 'ember';
|
||||
import startApp from '../helpers/start-app';
|
||||
import { authenticateSession, currentSession, invalidateSession } from 'ghost/tests/helpers/ember-simple-auth';
|
||||
import Mirage from 'ember-cli-mirage';
|
||||
import windowProxy from 'ghost/utils/window-proxy';
|
||||
|
||||
const {run} = Ember;
|
||||
|
||||
describe('Acceptance: Authentication', function () {
|
||||
let application,
|
||||
originalReplaceLocation;
|
||||
|
||||
beforeEach(function () {
|
||||
application = startApp();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
run(application, 'destroy');
|
||||
});
|
||||
|
||||
describe('general page', function () {
|
||||
beforeEach(function () {
|
||||
originalReplaceLocation = windowProxy.replaceLocation;
|
||||
windowProxy.replaceLocation = function (url) {
|
||||
visit(url);
|
||||
};
|
||||
|
||||
server.loadFixtures();
|
||||
const role = server.create('role', {name: 'Administrator'}),
|
||||
user = server.create('user', {roles: [role], slug: 'test-user'});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
windowProxy.replaceLocation = originalReplaceLocation;
|
||||
});
|
||||
|
||||
it('invalidates session on 401 API response', function () {
|
||||
const role = server.create('role', {name: 'Administrator'}),
|
||||
user = server.create('user', {roles: [role]});
|
||||
|
||||
// return a 401 when attempting to retrieve tags
|
||||
server.get('/users/', (db, request) => {
|
||||
return new Mirage.Response(401, {}, {
|
||||
errors: [
|
||||
{message: 'Access denied.', errorType: 'UnauthorizedError'}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
authenticateSession(application);
|
||||
visit('/team');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'url after 401').to.equal('/signin');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('editor', function () {
|
||||
let origDebounce = Ember.run.debounce;
|
||||
let origThrottle = Ember.run.throttle;
|
||||
|
||||
// we don't want the autosave interfering in this test
|
||||
beforeEach(function () {
|
||||
Ember.run.debounce = function () { };
|
||||
Ember.run.throttle = function () { };
|
||||
});
|
||||
|
||||
it('displays re-auth modal attempting to save with invalid session', function () {
|
||||
const role = server.create('role', {name: 'Administrator'}),
|
||||
user = server.create('user', {roles: [role]});
|
||||
|
||||
// simulate an invalid session when saving the edited post
|
||||
server.put('/posts/:id/', (db, request) => {
|
||||
let post = db.posts.find(request.params.id),
|
||||
[attrs] = JSON.parse(request.requestBody).posts;
|
||||
|
||||
if (attrs.markdown === 'Edited post body') {
|
||||
return new Mirage.Response(401, {}, {
|
||||
errors: [
|
||||
{message: 'Access denied.', errorType: 'UnauthorizedError'}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
posts: [post]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.loadFixtures();
|
||||
authenticateSession(application);
|
||||
|
||||
visit('/editor');
|
||||
|
||||
// create the post
|
||||
fillIn('#entry-title', 'Test Post');
|
||||
fillIn('textarea.markdown-editor', 'Test post body');
|
||||
click('.js-publish-button');
|
||||
|
||||
andThen(() => {
|
||||
// we shouldn't have a modal at this point
|
||||
expect(find('.modal-container #login').length, 'modal exists').to.equal(0);
|
||||
// we also shouldn't have any alerts
|
||||
expect(find('.gh-alert').length, 'no of alerts').to.equal(0);
|
||||
});
|
||||
|
||||
// update the post
|
||||
fillIn('textarea.markdown-editor', 'Edited post body');
|
||||
click('.js-publish-button');
|
||||
|
||||
andThen(() => {
|
||||
// we should see a re-auth modal
|
||||
expect(find('.modal-container #login').length, 'modal exists').to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
// don't clobber debounce/throttle for future tests
|
||||
afterEach(function () {
|
||||
Ember.run.debounce = origDebounce;
|
||||
Ember.run.throttle = origThrottle;
|
||||
});
|
||||
});
|
||||
});
|
|
@ -79,7 +79,7 @@ describe('Acceptance: Settings - Tags', function () {
|
|||
// TODO: this should always be run for acceptance tests
|
||||
server.loadFixtures();
|
||||
|
||||
authenticateSession(application);
|
||||
return authenticateSession(application);
|
||||
});
|
||||
|
||||
it('it renders, can be navigated, can edit, create & delete tags', function () {
|
||||
|
@ -138,7 +138,9 @@ describe('Acceptance: Settings - Tags', function () {
|
|||
keydown(38);
|
||||
keyup(38);
|
||||
});
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
// it navigates to previous tag
|
||||
expect(currentURL(), 'url after keyboard up arrow').to.equal(`/settings/tags/${tag1.slug}`);
|
||||
|
||||
|
@ -153,7 +155,9 @@ describe('Acceptance: Settings - Tags', function () {
|
|||
keydown(40);
|
||||
keyup(40);
|
||||
});
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
// it navigates to previous tag
|
||||
expect(currentURL(), 'url after keyboard down arrow').to.equal(`/settings/tags/${tag2.slug}`);
|
||||
|
||||
|
|
51
core/client/tests/fixtures/settings.js
vendored
51
core/client/tests/fixtures/settings.js
vendored
|
@ -1,51 +0,0 @@
|
|||
export default [{
|
||||
created_at: '2015-09-11T09:44:30.805Z',
|
||||
created_by: 1,
|
||||
id: 5,
|
||||
key: 'title',
|
||||
type: 'blog',
|
||||
updated_at: '2015-10-04T16:26:05.195Z',
|
||||
updated_by: 1,
|
||||
uuid: '39e16daf-43fa-4bf0-87d4-44948ba8bf4c',
|
||||
value: 'Test Blog'
|
||||
}, {
|
||||
created_at: '2015-09-11T09:44:30.806Z',
|
||||
created_by: 1,
|
||||
id: 6,
|
||||
key: 'description',
|
||||
type: 'blog',
|
||||
updated_at: '2015-10-04T16:26:05.198Z',
|
||||
updated_by: 1,
|
||||
uuid: 'e6c8b636-6925-4c4a-a5d9-1dc0870fb8ea',
|
||||
value: 'Thoughts, stories and ideas.'
|
||||
}, {
|
||||
created_at: '2015-09-11T09:44:30.809Z',
|
||||
created_by: 1,
|
||||
id: 10,
|
||||
key: 'postsPerPage',
|
||||
type: 'blog',
|
||||
updated_at: '2015-10-04T16:26:05.211Z',
|
||||
updated_by: 1,
|
||||
uuid: '775e6ca1-bcc3-4347-a53d-15d5d76c04a4',
|
||||
value: '5'
|
||||
}, {
|
||||
created_at: '2015-09-11T09:44:30.809Z',
|
||||
created_by: 1,
|
||||
id: 13,
|
||||
key: 'ghost_head',
|
||||
type: 'blog',
|
||||
updated_at: '2015-09-23T13:32:49.858Z',
|
||||
updated_by: 1,
|
||||
uuid: 'df7f3151-bc08-4a77-be9d-dd315b630d51',
|
||||
value: ''
|
||||
}, {
|
||||
created_at: '2015-09-11T09:44:30.809Z',
|
||||
created_by: 1,
|
||||
id: 14,
|
||||
key: 'ghost_foot',
|
||||
type: 'blog',
|
||||
updated_at: '2015-09-23T13:32:49.858Z',
|
||||
updated_by: 1,
|
||||
uuid: '0649d45e-828b-4dd0-8381-3dff6d1d5ddb',
|
||||
value: ''
|
||||
}];
|
42
core/client/tests/unit/mixins/validation-engine-test.js
Normal file
42
core/client/tests/unit/mixins/validation-engine-test.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describe,
|
||||
it
|
||||
} from 'mocha';
|
||||
import Ember from 'ember';
|
||||
import ValidationEngineMixin from 'ghost/mixins/validation-engine';
|
||||
|
||||
describe('ValidationEngineMixin', function () {
|
||||
// Replace this with your real tests.
|
||||
// it('works', function () {
|
||||
// var ValidationEngineObject = Ember.Object.extend(ValidationEngineMixin);
|
||||
// var subject = ValidationEngineObject.create();
|
||||
// expect(subject).to.be.ok;
|
||||
// });
|
||||
|
||||
describe('#validate', function () {
|
||||
it('loads the correct validator');
|
||||
it('rejects if the validator doesn\'t exist');
|
||||
it('resolves with valid object');
|
||||
it('rejects with invalid object');
|
||||
it('clears all existing errors');
|
||||
|
||||
describe('with a specified property', function () {
|
||||
it('resolves with valid property');
|
||||
it('rejects with invalid property');
|
||||
it('adds property to hasValidated array');
|
||||
it('clears existing error on specified property');
|
||||
});
|
||||
|
||||
it('handles a passed in model');
|
||||
it('uses this.model if available');
|
||||
});
|
||||
|
||||
describe('#save', function () {
|
||||
it('calls validate');
|
||||
it('rejects with validation errors');
|
||||
it('calls object\'s #save if validation passes');
|
||||
it('skips validation if it\'s a deletion');
|
||||
});
|
||||
});
|
|
@ -22,6 +22,9 @@ const {run} = Ember,
|
|||
// TODO: These tests have way too much duplication, consider creating test
|
||||
// helpers for validations
|
||||
|
||||
// TODO: Move testing of validation-engine behaviour into validation-engine-test
|
||||
// and replace these tests with specific validator tests
|
||||
|
||||
describe('Unit: Validator: tag-settings', function () {
|
||||
it('validates all fields by default', function () {
|
||||
let tag = Tag.create({}),
|
||||
|
|
Loading…
Add table
Reference in a new issue