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:
Kevin Ansfield 2016-06-14 12:46:24 +01:00
parent b4cdc85a59
commit eb2a0359cf
15 changed files with 339 additions and 194 deletions

View File

@ -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
}
}

View File

@ -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');
});

View File

@ -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');
});

View File

@ -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: {

View File

@ -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);

View File

@ -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');
}
}
});

View File

@ -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 ------------------------------------------------------- */

View File

@ -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')
}]
}
});
});
},

View File

@ -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);
},

View File

@ -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() {

View File

@ -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}}

View File

@ -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;
}

View File

@ -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();
}
}
});

View File

@ -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;
});
});
});

View File

@ -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 () {