Refactor error handling
closes https://github.com/TryGhost/Ghost/issues/6974 - update "change password" fields/process to use inline validations - remove `notifications.showErrors` and update all uses of it to `showAPIError` - display multiple API errors as alerts rather than toaster notifications - refactor `notifications.showAPIError` - remove `notifications.showErrors`, use a loop in `showAPIError` instead - properly determine the message from `AjaxError` or `AdapterError` objects - determine a unique key if possible so that we don't lose multiple different alerts - add `ServerUnreachable` error for when we get a status code of 0 (eg, when the ghost service has been shut down) - simplify error messages for our custom ajax errors
This commit is contained in:
parent
b4cdc85a59
commit
eb2a0359cf
@ -46,7 +46,6 @@ export default RESTAdapter.extend(DataAdapterMixin, {
|
||||
if (status === 401) {
|
||||
if (this.get('session.isAuthenticated')) {
|
||||
this.get('session').invalidate();
|
||||
return; // prevent error from bubbling because invalidate is async
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,8 +29,8 @@ export default ModalComponent.extend({
|
||||
this.get('routing').transitionTo('posts');
|
||||
},
|
||||
|
||||
_failure() {
|
||||
this.get('notifications').showAlert('Your post could not be deleted. Please try again.', {type: 'error', key: 'post.delete.failed'});
|
||||
_failure(error) {
|
||||
this.get('notifications').showAPIError(error, {key: 'post.delete.failed'});
|
||||
},
|
||||
|
||||
actions: {
|
||||
@ -39,8 +39,8 @@ export default ModalComponent.extend({
|
||||
|
||||
this._deletePost().then(() => {
|
||||
this._success();
|
||||
}, () => {
|
||||
this._failure();
|
||||
}, (error) => {
|
||||
this._failure(error);
|
||||
}).finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import RSVP from 'rsvp';
|
||||
import injectService from 'ember-service/inject';
|
||||
import {A as emberA, isEmberArray} from 'ember-array/utils';
|
||||
import {A as emberA} from 'ember-array/utils';
|
||||
import run from 'ember-runloop';
|
||||
import ModalComponent from 'ghost-admin/components/modals/base';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
@ -111,13 +111,9 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||
} else {
|
||||
notifications.showNotification(notificationText, {key: 'invite.send.success'});
|
||||
}
|
||||
}).catch((errors) => {
|
||||
}).catch((error) => {
|
||||
newUser.deleteRecord();
|
||||
if (isEmberArray(errors)) {
|
||||
notifications.showErrors(errors, {key: 'invite.send'});
|
||||
} else {
|
||||
notifications.showAPIError(errors, {key: 'invite.send'});
|
||||
}
|
||||
notifications.showAPIError(error, {key: 'invite.send'});
|
||||
}).finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
|
@ -185,11 +185,8 @@ export default Controller.extend(SettingsMenuMixin, {
|
||||
}),
|
||||
|
||||
showErrors(errors) {
|
||||
if (isVersionMismatchError(errors)) {
|
||||
return this.get('notifications').showAPIError(errors);
|
||||
}
|
||||
errors = isEmberArray(errors) ? errors : [errors];
|
||||
this.get('notifications').showErrors(errors);
|
||||
this.get('notifications').showAPIError(errors);
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
@ -61,9 +61,7 @@ export default Controller.extend({
|
||||
this.set('isSaving', true);
|
||||
|
||||
return settings.save().catch((err) => {
|
||||
if (err && err.isAdapterError) {
|
||||
this.get('notifications').showAPIError(err);
|
||||
}
|
||||
this.get('notifications').showAPIError(err);
|
||||
throw err;
|
||||
}).finally(() => {
|
||||
this.set('isSaving', false);
|
||||
|
@ -12,6 +12,7 @@ import { invoke } from 'ember-invoke-action';
|
||||
|
||||
export default Controller.extend({
|
||||
submitting: false,
|
||||
updatingPassword: false,
|
||||
lastPromise: null,
|
||||
showDeleteUserModal: false,
|
||||
showTransferOwnerModal: false,
|
||||
@ -140,13 +141,12 @@ export default Controller.extend({
|
||||
this.get('notifications').closeAlerts('user.update');
|
||||
|
||||
return model;
|
||||
}).catch((errors) => {
|
||||
if (isEmberArray(errors)) {
|
||||
this.get('notifications').showErrors(errors, {key: 'user.update'});
|
||||
} else {
|
||||
this.get('notifications').showAPIError(errors);
|
||||
}).catch((error) => {
|
||||
// validation engine returns undefined so we have to check
|
||||
// before treating the failure as an API error
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error, {key: 'user.update'});
|
||||
}
|
||||
|
||||
this.toggleProperty('submitting');
|
||||
});
|
||||
|
||||
@ -168,11 +168,13 @@ export default Controller.extend({
|
||||
}
|
||||
},
|
||||
|
||||
password() {
|
||||
changePassword() {
|
||||
let user = this.get('user');
|
||||
|
||||
if (user.get('isPasswordValid')) {
|
||||
user.saveNewPassword().then((model) => {
|
||||
if (!this.get('updatingPassword')) {
|
||||
this.set('updatingPassword', true);
|
||||
|
||||
return user.saveNewPassword().then((model) => {
|
||||
// Clear properties from view
|
||||
user.setProperties({
|
||||
password: '',
|
||||
@ -180,15 +182,22 @@ export default Controller.extend({
|
||||
ne2Password: ''
|
||||
});
|
||||
|
||||
this.get('notifications').showAlert('Password updated.', {type: 'success', key: 'user.change-password.success'});
|
||||
this.get('notifications').showNotification('Password updated.', {type: 'success', key: 'user.change-password.success'});
|
||||
|
||||
// clear errors manually for ne2password because validation
|
||||
// engine only clears the "validated proeprty"
|
||||
// TODO: clean up once we have a better validations library
|
||||
user.get('errors').remove('ne2Password');
|
||||
|
||||
return model;
|
||||
}).catch((errors) => {
|
||||
this.get('notifications').showAPIError(errors, {key: 'user.change-password'});
|
||||
}).catch((error) => {
|
||||
// error will be undefined if we have a validation error
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error, {key: 'user.change-password'});
|
||||
}
|
||||
}).finally(() => {
|
||||
this.set('updatingPassword', false);
|
||||
});
|
||||
} else {
|
||||
// TODO: switch to in-line validation
|
||||
this.get('notifications').showErrors(user.get('passwordValidationErrors'), {key: 'user.change-password'});
|
||||
}
|
||||
},
|
||||
|
||||
@ -419,6 +428,26 @@ export default Controller.extend({
|
||||
|
||||
toggleUploadImageModal() {
|
||||
this.toggleProperty('showUploadImageModal');
|
||||
},
|
||||
|
||||
// TODO: remove those mutation actions once we have better
|
||||
// inline validations that auto-clear errors on input
|
||||
updatePassword(password) {
|
||||
this.set('user.password', password);
|
||||
this.get('user.hasValidated').removeObject('password');
|
||||
this.get('user.errors').remove('password');
|
||||
},
|
||||
|
||||
updateNewPassword(password) {
|
||||
this.set('user.newPassword', password);
|
||||
this.get('user.hasValidated').removeObject('newPassword');
|
||||
this.get('user.errors').remove('newPassword');
|
||||
},
|
||||
|
||||
updateNe2Password(password) {
|
||||
this.set('user.ne2Password', password);
|
||||
this.get('user.hasValidated').removeObject('ne2Password');
|
||||
this.get('user.errors').remove('ne2Password');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -472,12 +472,19 @@ export function testConfig() {
|
||||
|
||||
this.put('/users/:id/', function (db, request) {
|
||||
let {id} = request.params;
|
||||
let [attrs] = JSON.parse(request.requestBody).users;
|
||||
let record = db.users.update(id, attrs);
|
||||
|
||||
return {
|
||||
user: record
|
||||
};
|
||||
if (id === 'password') {
|
||||
return {
|
||||
password: [{message: 'Password changed successfully.'}]
|
||||
};
|
||||
} else {
|
||||
let [attrs] = JSON.parse(request.requestBody).users;
|
||||
let record = db.users.update(id, attrs);
|
||||
|
||||
return {
|
||||
user: record
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/* External sites ------------------------------------------------------- */
|
||||
|
@ -1,10 +1,9 @@
|
||||
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
||||
import computed, {equal, empty} from 'ember-computed';
|
||||
import injectService from 'ember-service/inject';
|
||||
|
||||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
import { hasMany } from 'ember-data/relationships';
|
||||
import {hasMany} from 'ember-data/relationships';
|
||||
import computed, {equal} from 'ember-computed';
|
||||
import injectService from 'ember-service/inject';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
|
||||
export default Model.extend(ValidationEngine, {
|
||||
@ -39,6 +38,7 @@ export default Model.extend(ValidationEngine, {
|
||||
|
||||
ghostPaths: injectService(),
|
||||
ajax: injectService(),
|
||||
session: injectService(),
|
||||
|
||||
// TODO: Once client-side permissions are in place,
|
||||
// remove the hard role check.
|
||||
@ -47,7 +47,9 @@ export default Model.extend(ValidationEngine, {
|
||||
isAdmin: equal('role.name', 'Administrator'),
|
||||
isOwner: equal('role.name', 'Owner'),
|
||||
|
||||
isPasswordValid: empty('passwordValidationErrors.[]'),
|
||||
isLoggedIn: computed('id', 'session.user.id', function () {
|
||||
return this.get('id') === this.get('session.user.id');
|
||||
}),
|
||||
|
||||
active: computed('status', function () {
|
||||
return ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked'].indexOf(this.get('status')) > -1;
|
||||
@ -59,20 +61,6 @@ export default Model.extend(ValidationEngine, {
|
||||
|
||||
pending: equal('status', 'invited-pending'),
|
||||
|
||||
passwordValidationErrors: computed('password', 'newPassword', 'ne2Password', function () {
|
||||
let validationErrors = [];
|
||||
|
||||
if (!validator.equals(this.get('newPassword'), this.get('ne2Password'))) {
|
||||
validationErrors.push({message: 'Your new passwords do not match'});
|
||||
}
|
||||
|
||||
if (!validator.isLength(this.get('newPassword'), 8)) {
|
||||
validationErrors.push({message: 'Your password is not long enough. It must be at least 8 characters long.'});
|
||||
}
|
||||
|
||||
return validationErrors;
|
||||
}),
|
||||
|
||||
role: computed('roles', {
|
||||
get() {
|
||||
return this.get('roles.firstObject');
|
||||
@ -88,16 +76,19 @@ export default Model.extend(ValidationEngine, {
|
||||
|
||||
saveNewPassword() {
|
||||
let url = this.get('ghostPaths.url').api('users', 'password');
|
||||
let validation = this.get('isLoggedIn') ? 'ownPasswordChange' : 'passwordChange';
|
||||
|
||||
return this.get('ajax').put(url, {
|
||||
data: {
|
||||
password: [{
|
||||
user_id: this.get('id'),
|
||||
oldPassword: this.get('password'),
|
||||
newPassword: this.get('newPassword'),
|
||||
ne2Password: this.get('ne2Password')
|
||||
}]
|
||||
}
|
||||
return this.validate({property: validation}).then(() => {
|
||||
return this.get('ajax').put(url, {
|
||||
data: {
|
||||
password: [{
|
||||
user_id: this.get('id'),
|
||||
oldPassword: this.get('password'),
|
||||
newPassword: this.get('newPassword'),
|
||||
ne2Password: this.get('ne2Password')
|
||||
}]
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -26,8 +26,22 @@ export function isVersionMismatchError(errorOrStatus, payload) {
|
||||
|
||||
/* Request entity too large error */
|
||||
|
||||
export function ServerUnreachableError(errors) {
|
||||
AjaxError.call(this, errors, 'Server was unreachable');
|
||||
}
|
||||
|
||||
ServerUnreachableError.prototype = Object.create(AjaxError.prototype);
|
||||
|
||||
export function isServerUnreachableError(error) {
|
||||
if (isAjaxError(error)) {
|
||||
return error instanceof ServerUnreachableError;
|
||||
} else {
|
||||
return error === 0 || error === '0';
|
||||
}
|
||||
}
|
||||
|
||||
export function RequestEntityTooLargeError(errors) {
|
||||
AjaxError.call(this, errors, 'Request was rejected because it\'s larger than the maximum file size the server allows');
|
||||
AjaxError.call(this, errors, 'Request is larger than the maximum file size the server allows');
|
||||
}
|
||||
|
||||
RequestEntityTooLargeError.prototype = Object.create(AjaxError.prototype);
|
||||
@ -43,7 +57,7 @@ export function isRequestEntityTooLargeError(errorOrStatus) {
|
||||
/* Unsupported media type error */
|
||||
|
||||
export function UnsupportedMediaTypeError(errors) {
|
||||
AjaxError.call(this, errors, 'Request was rejected because it contains an unknown or unsupported file type.');
|
||||
AjaxError.call(this, errors, 'Request contains an unknown or unsupported file type.');
|
||||
}
|
||||
|
||||
UnsupportedMediaTypeError.prototype = Object.create(AjaxError.prototype);
|
||||
@ -79,6 +93,8 @@ export default AjaxService.extend({
|
||||
handleResponse(status, headers, payload) {
|
||||
if (this.isVersionMismatchError(status, headers, payload)) {
|
||||
return new VersionMismatchError(payload.errors);
|
||||
} else if (this.isServerUnreachableError(status, headers, payload)) {
|
||||
return new ServerUnreachableError(payload.errors);
|
||||
} else if (this.isRequestEntityTooLargeError(status, headers, payload)) {
|
||||
return new RequestEntityTooLargeError(payload.errors);
|
||||
} else if (this.isUnsupportedMediaTypeError(status, headers, payload)) {
|
||||
@ -110,6 +126,10 @@ export default AjaxService.extend({
|
||||
return isVersionMismatchError(status, payload);
|
||||
},
|
||||
|
||||
isServerUnreachableError(status/*, headers, payload */) {
|
||||
return isServerUnreachableError(status);
|
||||
},
|
||||
|
||||
isRequestEntityTooLargeError(status/*, headers, payload */) {
|
||||
return isRequestEntityTooLargeError(status);
|
||||
},
|
||||
|
@ -5,6 +5,8 @@ import get from 'ember-metal/get';
|
||||
import set from 'ember-metal/set';
|
||||
import injectService from 'ember-service/inject';
|
||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||
import {isBlank} from 'ember-utils';
|
||||
import {dasherize} from 'ember-string';
|
||||
|
||||
// Notification keys take the form of "noun.verb.message", eg:
|
||||
//
|
||||
@ -82,25 +84,6 @@ export default Service.extend({
|
||||
}, options.delayed);
|
||||
},
|
||||
|
||||
// TODO: review whether this can be removed once no longer used by validations
|
||||
showErrors(errors, options) {
|
||||
options = options || {};
|
||||
options.type = options.type || 'error';
|
||||
// TODO: getting keys from the server would be useful here (necessary for i18n)
|
||||
options.key = (options.key && `${options.key}.api-error`) || 'api-error';
|
||||
|
||||
if (!options.doNotCloseNotifications) {
|
||||
this.closeNotifications();
|
||||
}
|
||||
|
||||
// ensure all errors that are passed in get shown
|
||||
options.doNotCloseNotifications = true;
|
||||
|
||||
for (let i = 0; i < errors.length; i += 1) {
|
||||
this.showNotification(errors[i].message || errors[i], options);
|
||||
}
|
||||
},
|
||||
|
||||
showAPIError(resp, options) {
|
||||
// handle "global" errors
|
||||
if (isVersionMismatchError(resp)) {
|
||||
@ -120,24 +103,26 @@ export default Service.extend({
|
||||
_showAPIError(resp, options) {
|
||||
options = options || {};
|
||||
options.type = options.type || 'error';
|
||||
// TODO: getting keys from the server would be useful here (necessary for i18n)
|
||||
options.key = (options.key && `${options.key}.api-error`) || 'api-error';
|
||||
|
||||
if (!options.doNotCloseNotifications) {
|
||||
this.closeNotifications();
|
||||
// if possible use the title to get a unique key
|
||||
// - we only show one alert for each key so if we get multiple errors
|
||||
// only the last one will be shown
|
||||
if (!options.key && !isBlank(get(resp, 'title'))) {
|
||||
options.key = dasherize(get(resp, 'title'));
|
||||
}
|
||||
options.key = ['api-error', options.key].compact().join('.');
|
||||
|
||||
let msg = options.defaultErrorText || 'There was a problem on the server, please try again.';
|
||||
|
||||
if (resp instanceof String) {
|
||||
msg = resp;
|
||||
} else if (!isBlank(get(resp, 'detail'))) {
|
||||
msg = resp.detail;
|
||||
} else if (!isBlank(get(resp, 'message'))) {
|
||||
msg = resp.message;
|
||||
}
|
||||
|
||||
options.defaultErrorText = options.defaultErrorText || 'There was a problem on the server, please try again.';
|
||||
|
||||
if (resp && isEmberArray(resp) && resp.length) { // Array of errors
|
||||
this.showErrors(resp, options);
|
||||
} else if (resp && resp.message) {
|
||||
this.showAlert(resp.message, options);
|
||||
} else if (resp && resp.detail) { // ember-ajax provided error message
|
||||
this.showAlert(resp.detail, options);
|
||||
} else { // text error or no error
|
||||
this.showAlert(resp || options.defaultErrorText, options);
|
||||
}
|
||||
this.showAlert(msg, options);
|
||||
},
|
||||
|
||||
displayDelayed() {
|
||||
|
@ -171,24 +171,27 @@
|
||||
{{#unless isAdminUserOnOwnerProfile}}
|
||||
<fieldset>
|
||||
{{#unless isNotOwnProfile}}
|
||||
<div class="form-group">
|
||||
<label for="user-password-old">Old Password</label>
|
||||
{{gh-input user.password type="password" id="user-password-old" update=(action (mut user.password))}}
|
||||
</div>
|
||||
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="password"}}
|
||||
<label for="user-password-old">Old Password</label>
|
||||
{{gh-input value=user.password type="password" id="user-password-old" update=(action 'updatePassword') onenter=(action "changePassword")}}
|
||||
{{gh-error-message errors=user.errors property="password"}}
|
||||
{{/gh-form-group}}
|
||||
{{/unless}}
|
||||
|
||||
<div class="form-group">
|
||||
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="newPassword"}}
|
||||
<label for="user-password-new">New Password</label>
|
||||
{{gh-input user.newPassword type="password" id="user-password-new" update=(action (mut user.newPassword))}}
|
||||
</div>
|
||||
{{gh-input user.newPassword type="password" id="user-password-new" update=(action 'updateNewPassword') onenter=(action "changePassword")}}
|
||||
{{gh-error-message errors=user.errors property="newPassword"}}
|
||||
{{/gh-form-group}}
|
||||
|
||||
<div class="form-group">
|
||||
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="ne2Password"}}
|
||||
<label for="user-new-password-verification">Verify Password</label>
|
||||
{{gh-input user.ne2Password type="password" id="user-new-password-verification" update=(action (mut user.ne2Password))}}
|
||||
</div>
|
||||
{{gh-input user.ne2Password type="password" id="user-new-password-verification" update=(action 'updateNe2Password') onenter=(action "changePassword")}}
|
||||
{{gh-error-message errors=user.errors property="ne2Password"}}
|
||||
{{/gh-form-group}}
|
||||
|
||||
<div class="form-group">
|
||||
<button type="button" class="btn btn-red button-change-password" {{action "password"}}>Change Password</button>
|
||||
{{#gh-spin-button class="btn btn-red button-change-password" action=(action "changePassword") submitting=updatingPassword}}Change Password{{/gh-spin-button}}
|
||||
</div>
|
||||
</fieldset>
|
||||
{{/unless}}
|
||||
|
@ -1,47 +0,0 @@
|
||||
import {isEmberArray} from 'ember-array/utils';
|
||||
|
||||
// TODO: this should be removed and instead have our app serializer properly
|
||||
// process the response so that errors can be tied to the model
|
||||
|
||||
// Used in API request fail handlers to parse a standard api error
|
||||
// response json for the message to display
|
||||
export default function getRequestErrorMessage(request, performConcat) {
|
||||
let message,
|
||||
msgDetail;
|
||||
|
||||
// Can't really continue without a request
|
||||
if (!request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Seems like a sensible default
|
||||
message = request.statusText;
|
||||
|
||||
// If a non 200 response
|
||||
if (request.status !== 200) {
|
||||
try {
|
||||
// Try to parse out the error, or default to 'Unknown'
|
||||
if (request.errors && isEmberArray(request.errors)) {
|
||||
message = request.errors.map((errorItem) => {
|
||||
return errorItem.message;
|
||||
});
|
||||
} else {
|
||||
message = request.error || 'Unknown Error';
|
||||
}
|
||||
} catch (e) {
|
||||
msgDetail = request.status ? `${request.status} - ${request.statusText}` : 'Server was not available';
|
||||
message = `The server returned an error (${msgDetail}).`;
|
||||
}
|
||||
}
|
||||
|
||||
if (performConcat && isEmberArray(message)) {
|
||||
message = message.join('<br />');
|
||||
}
|
||||
|
||||
// return an array of errors by default
|
||||
if (!performConcat && typeof message === 'string') {
|
||||
message = [message];
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import BaseValidator from './base';
|
||||
import {isBlank} from 'ember-utils';
|
||||
|
||||
export default BaseValidator.create({
|
||||
properties: ['name', 'bio', 'email', 'location', 'website', 'roles'],
|
||||
@ -77,5 +78,45 @@ export default BaseValidator.create({
|
||||
this.invalidate();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
passwordChange(model) {
|
||||
let newPassword = model.get('newPassword');
|
||||
let ne2Password = model.get('ne2Password');
|
||||
|
||||
// validation only marks the requested property as validated so we
|
||||
// have to add properties manually
|
||||
model.get('hasValidated').addObject('newPassword');
|
||||
model.get('hasValidated').addObject('ne2Password');
|
||||
|
||||
if (isBlank(newPassword) && isBlank(ne2Password)) {
|
||||
model.get('errors').add('newPassword', 'Sorry, passwords can\'t be blank');
|
||||
this.invalidate();
|
||||
} else {
|
||||
if (!validator.equals(newPassword, ne2Password)) {
|
||||
model.get('errors').add('ne2Password', 'Your new passwords do not match');
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
if (!validator.isLength(newPassword, 8)) {
|
||||
model.get('errors').add('newPassword', 'Your password must be at least 8 characters long.');
|
||||
this.invalidate();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ownPasswordChange(model) {
|
||||
let oldPassword = model.get('password');
|
||||
|
||||
this.passwordChange(model);
|
||||
|
||||
// validation only marks the requested property as validated so we
|
||||
// have to add properties manually
|
||||
model.get('hasValidated').addObject('password');
|
||||
|
||||
if (isBlank(oldPassword)) {
|
||||
model.get('errors').add('password', 'Your current password is required to set a new one');
|
||||
this.invalidate();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ import destroyApp from '../helpers/destroy-app';
|
||||
import { invalidateSession, authenticateSession } from 'ghost-admin/tests/helpers/ember-simple-auth';
|
||||
import { errorOverride, errorReset } from 'ghost-admin/tests/helpers/adapter-error';
|
||||
import Mirage from 'ember-cli-mirage';
|
||||
import $ from 'jquery';
|
||||
|
||||
describe('Acceptance: Team', function () {
|
||||
let application;
|
||||
@ -61,9 +62,11 @@ describe('Acceptance: Team', function () {
|
||||
});
|
||||
|
||||
describe('when logged in', function () {
|
||||
let admin;
|
||||
|
||||
beforeEach(function () {
|
||||
let role = server.create('role', {name: 'Admininstrator'});
|
||||
let user = server.create('user', {roles: [role]});
|
||||
admin = server.create('user', {roles: [role]});
|
||||
|
||||
server.loadFixtures();
|
||||
|
||||
@ -238,7 +241,7 @@ describe('Acceptance: Team', function () {
|
||||
let user;
|
||||
|
||||
beforeEach(function () {
|
||||
server.create('user', {
|
||||
user = server.create('user', {
|
||||
slug: 'test-1',
|
||||
name: 'Test User',
|
||||
facebook: 'test',
|
||||
@ -456,6 +459,133 @@ describe('Acceptance: Team', function () {
|
||||
andThen(() => {
|
||||
expect(find('.user-details-bottom .form-group:nth-of-type(7)').hasClass('error'), 'bio input should be in error state').to.be.true;
|
||||
});
|
||||
|
||||
// password reset ------
|
||||
|
||||
// button triggers validation
|
||||
click('.button-change-password');
|
||||
|
||||
andThen(() => {
|
||||
expect(
|
||||
find('#user-password-new').closest('.form-group').hasClass('error'),
|
||||
'new password has error class when blank'
|
||||
).to.be.true;
|
||||
|
||||
expect(
|
||||
find('#user-password-new').siblings('.response').text(),
|
||||
'new password error when blank'
|
||||
).to.match(/can't be blank/);
|
||||
});
|
||||
|
||||
// typing in inputs clears validation
|
||||
fillIn('#user-password-new', 'password');
|
||||
triggerEvent('#user-password-new', 'input');
|
||||
|
||||
andThen(() => {
|
||||
expect(
|
||||
find('#user-password-new').closest('.form-group').hasClass('error'),
|
||||
'password validation is visible after typing'
|
||||
).to.be.false;
|
||||
});
|
||||
|
||||
// enter key triggers action
|
||||
keyEvent('#user-password-new', 'keyup', 13);
|
||||
|
||||
andThen(() => {
|
||||
expect(
|
||||
find('#user-new-password-verification').closest('.form-group').hasClass('error'),
|
||||
'confirm password has error class when it doesn\'t match'
|
||||
).to.be.true;
|
||||
|
||||
expect(
|
||||
find('#user-new-password-verification').siblings('.response').text(),
|
||||
'confirm password error when it doesn\'t match'
|
||||
).to.match(/do not match/);
|
||||
});
|
||||
|
||||
// submits with correct details
|
||||
fillIn('#user-new-password-verification', 'password');
|
||||
click('.button-change-password');
|
||||
|
||||
andThen(() => {
|
||||
// hits the endpoint
|
||||
let [lastRequest] = server.pretender.handledRequests.slice(-1);
|
||||
let params = $.deparam(lastRequest.requestBody);
|
||||
|
||||
expect(lastRequest.url, 'password request URL')
|
||||
.to.match(/\/users\/password/);
|
||||
|
||||
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
||||
expect(params.password[0].user_id).to.equal(user.id.toString());
|
||||
expect(params.password[0].newPassword).to.equal('password');
|
||||
expect(params.password[0].ne2Password).to.equal('password');
|
||||
/* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */
|
||||
|
||||
// clears the fields
|
||||
expect(
|
||||
find('#user-password-new').val(),
|
||||
'password field after submit'
|
||||
).to.be.blank;
|
||||
|
||||
expect(
|
||||
find('#user-new-password-verification').val(),
|
||||
'password verification field after submit'
|
||||
).to.be.blank;
|
||||
|
||||
// displays a notification
|
||||
expect(
|
||||
find('.gh-notifications .gh-notification').length,
|
||||
'password saved notification is displayed'
|
||||
).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('own user', function () {
|
||||
beforeEach(function () {
|
||||
server.loadFixtures();
|
||||
});
|
||||
|
||||
it('requires current password when changing password', function () {
|
||||
visit(`/team/${admin.slug}`);
|
||||
|
||||
// test the "old password" field is validated
|
||||
click('.button-change-password');
|
||||
|
||||
andThen(() => {
|
||||
// old password has error
|
||||
expect(
|
||||
find('#user-password-old').closest('.form-group').hasClass('error'),
|
||||
'old password has error class when blank'
|
||||
).to.be.true;
|
||||
|
||||
expect(
|
||||
find('#user-password-old').siblings('.response').text(),
|
||||
'old password error when blank'
|
||||
).to.match(/is required/);
|
||||
|
||||
// new password has error
|
||||
expect(
|
||||
find('#user-password-new').closest('.form-group').hasClass('error'),
|
||||
'new password has error class when blank'
|
||||
).to.be.true;
|
||||
|
||||
expect(
|
||||
find('#user-password-new').siblings('.response').text(),
|
||||
'new password error when blank'
|
||||
).to.match(/can't be blank/);
|
||||
});
|
||||
|
||||
// validation is cleared when typing
|
||||
fillIn('#user-password-old', 'password');
|
||||
triggerEvent('#user-password-old', 'input');
|
||||
|
||||
andThen(() => {
|
||||
expect(
|
||||
find('#user-password-old').closest('.form-group').hasClass('error'),
|
||||
'old password validation is in error state after typing'
|
||||
).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import {AjaxError, InvalidError} from 'ember-ajax/errors';
|
||||
import {ServerUnreachableError} from 'ghost-admin/services/ajax';
|
||||
|
||||
describeModule(
|
||||
'service:notifications',
|
||||
@ -149,23 +150,6 @@ describeModule(
|
||||
expect(notifications.get('notifications.length')).to.equal(2);
|
||||
});
|
||||
|
||||
// TODO: review whether this can be removed once it's no longer used by validations
|
||||
it('#showErrors adds multiple notifications', function () {
|
||||
let notifications = this.subject();
|
||||
|
||||
run(() => {
|
||||
notifications.showErrors([
|
||||
{message: 'First'},
|
||||
{message: 'Second'}
|
||||
]);
|
||||
});
|
||||
|
||||
expect(notifications.get('notifications')).to.deep.equal([
|
||||
{message: 'First', status: 'notification', type: 'error', key: undefined},
|
||||
{message: 'Second', status: 'notification', type: 'error', key: undefined}
|
||||
]);
|
||||
});
|
||||
|
||||
it('#showAPIError handles single json response error', function () {
|
||||
let notifications = this.subject();
|
||||
let error = new AjaxError([{message: 'Single error'}]);
|
||||
@ -181,24 +165,21 @@ describeModule(
|
||||
expect(get(alert, 'key')).to.equal('api-error');
|
||||
});
|
||||
|
||||
// TODO: update once we have unique api key handling
|
||||
it('#showAPIError handles multiple json response errors', function () {
|
||||
let notifications = this.subject();
|
||||
let error = new AjaxError([
|
||||
{message: 'First error'},
|
||||
{message: 'Second error'}
|
||||
{title: 'First error', message: 'First error message'},
|
||||
{title: 'Second error', message: 'Second error message'}
|
||||
]);
|
||||
|
||||
run(() => {
|
||||
notifications.showAPIError(error);
|
||||
});
|
||||
|
||||
// First error is removed due to duplicate api-key
|
||||
let alert = notifications.get('alerts.firstObject');
|
||||
expect(get(alert, 'message')).to.equal('Second error');
|
||||
expect(get(alert, 'status')).to.equal('alert');
|
||||
expect(get(alert, 'type')).to.equal('error');
|
||||
expect(get(alert, 'key')).to.equal('api-error');
|
||||
expect(notifications.get('alerts.length')).to.equal(2);
|
||||
let [alert1, alert2] = notifications.get('alerts');
|
||||
expect(alert1).to.deep.equal({message: 'First error message', status: 'alert', type: 'error', key: 'api-error.first-error'});
|
||||
expect(alert2).to.deep.equal({message: 'Second error message', status: 'alert', type: 'error', key: 'api-error.second-error'});
|
||||
});
|
||||
|
||||
it('#showAPIError displays default error text if response has no error/message', function () {
|
||||
@ -228,7 +209,7 @@ describeModule(
|
||||
notifications.showAPIError('Test', {key: 'test.alert'});
|
||||
});
|
||||
|
||||
expect(notifications.get('alerts.firstObject.key')).to.equal('test.alert.api-error');
|
||||
expect(notifications.get('alerts.firstObject.key')).to.equal('api-error.test.alert');
|
||||
});
|
||||
|
||||
it('#showAPIError sets correct key when not passed a key', function () {
|
||||
@ -241,19 +222,34 @@ describeModule(
|
||||
expect(notifications.get('alerts.firstObject.key')).to.equal('api-error');
|
||||
});
|
||||
|
||||
it('#showAPIError parses errors from ember-ajax correctly', function () {
|
||||
it('#showAPIError parses default ember-ajax errors correctly', function () {
|
||||
let notifications = this.subject();
|
||||
let error = new InvalidError([{message: 'Test Error'}]);
|
||||
let error = new InvalidError();
|
||||
|
||||
run(() => {
|
||||
notifications.showAPIError(error);
|
||||
});
|
||||
|
||||
let notification = notifications.get('alerts.firstObject');
|
||||
expect(get(notification, 'message')).to.equal('Test Error');
|
||||
expect(get(notification, 'message')).to.equal('Request was rejected because it was invalid');
|
||||
expect(get(notification, 'status')).to.equal('alert');
|
||||
expect(get(notification, 'type')).to.equal('error');
|
||||
expect(get(notification, 'key')).to.equal('api-error');
|
||||
expect(get(notification, 'key')).to.equal('api-error.ajax-error');
|
||||
});
|
||||
|
||||
it('#showAPIError parses custom ember-ajax errors correctly', function () {
|
||||
let notifications = this.subject();
|
||||
let error = new ServerUnreachableError();
|
||||
|
||||
run(() => {
|
||||
notifications.showAPIError(error);
|
||||
});
|
||||
|
||||
let notification = notifications.get('alerts.firstObject');
|
||||
expect(get(notification, 'message')).to.equal('Server was unreachable');
|
||||
expect(get(notification, 'status')).to.equal('alert');
|
||||
expect(get(notification, 'type')).to.equal('error');
|
||||
expect(get(notification, 'key')).to.equal('api-error.ajax-error');
|
||||
});
|
||||
|
||||
it('#displayDelayed moves delayed notifications into content', function () {
|
||||
|
Loading…
Reference in New Issue
Block a user