0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Agressive stripping of the model attributes

- fixes #517
- prevents this from occuring again in future with other relations
- validation function & stripping done for all models
- casper test for flow, plus validation & logged out tests
This commit is contained in:
Hannah Wolfe 2013-08-25 11:49:31 +01:00
parent 41e36cca7e
commit c70dfde7e3
11 changed files with 296 additions and 10 deletions

View file

@ -7,6 +7,25 @@ var GhostBookshelf = require('./base'),
Permission = GhostBookshelf.Model.extend({
tableName: 'permissions',
permittedAttributes: ['id', 'name', 'object_type', 'action_type', 'object_id'],
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
// TODO: validate object_type, action_type and object_id
GhostBookshelf.validator.check(this.get('name'), "Permission name cannot be blank").notEmpty();
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
},
roles: function () {
return this.belongsToMany(Role);
},

View file

@ -13,6 +13,12 @@ Post = GhostBookshelf.Model.extend({
tableName: 'posts',
permittedAttributes: [
'id', 'uuid', 'title', 'slug', 'content_raw', 'content', 'meta_title', 'meta_description', 'meta_keywords',
'featured', 'image', 'status', 'language', 'author_id', 'created_at', 'created_by', 'updated_at', 'updated_by',
'published_at', 'published_by'
],
hasTimestamps: true,
defaults: function () {
@ -24,9 +30,9 @@ Post = GhostBookshelf.Model.extend({
},
initialize: function () {
this.on('saving', this.validate, this);
this.on('creating', this.creating, this);
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
@ -36,6 +42,11 @@ Post = GhostBookshelf.Model.extend({
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
this.set('content', converter.makeHtml(this.get('content_raw')));
if (this.hasChanged('status') && this.get('status') === 'published') {

View file

@ -7,6 +7,25 @@ var User = require('./user').User,
Role = GhostBookshelf.Model.extend({
tableName: 'roles',
permittedAttributes: ['id', 'name', 'description'],
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
GhostBookshelf.validator.check(this.get('name'), "Role name cannot be blank").notEmpty();
GhostBookshelf.validator.check(this.get('description'), "Role description cannot be blank").notEmpty();
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
},
users: function () {
return this.belongsToMany(User);
},

View file

@ -8,13 +8,36 @@ var Settings,
// Each setting is saved as a separate row in the database,
// but the overlying API treats them as a single key:value mapping
Settings = GhostBookshelf.Model.extend({
tableName: 'settings',
hasTimestamps: true,
permittedAttributes: ['id', 'uuid', 'key', 'value', 'type', 'created_at', 'created_by', 'updated_at', 'update_by'],
defaults: function () {
return {
uuid: uuid.v4(),
type: 'general'
};
},
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
// TODO: validate value, check type is one of the allowed values etc
GhostBookshelf.validator.check(this.get('key'), "Setting key cannot be blank").notEmpty();
GhostBookshelf.validator.check(this.get('type'), "Setting type cannot be blank").notEmpty();
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
}
}, {
read: function (_key) {

View file

@ -34,6 +34,11 @@ User = GhostBookshelf.Model.extend({
hasTimestamps: true,
permittedAttributes: [
'id', 'uuid', 'full_name', 'password', 'email_address', 'profile_picture', 'cover_picture', 'bio', 'url', 'location',
'created_at', 'created_by', 'updated_at', 'updated_by'
],
defaults: function () {
return {
uuid: uuid.v4()
@ -41,6 +46,7 @@ User = GhostBookshelf.Model.extend({
},
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
@ -53,6 +59,13 @@ User = GhostBookshelf.Model.extend({
return true;
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
},
posts: function () {
return this.hasMany(Posts, 'created_by');
},

View file

@ -37,6 +37,7 @@ casper.test.begin("Can login to Ghost", 3, function suite(test) {
casper.waitFor(function checkOpaque() {
return this.evaluate(function () {
var loginBox = document.querySelector('.login-box');
return window.getComputedStyle(loginBox).getPropertyValue('display') === "block"
&& window.getComputedStyle(loginBox).getPropertyValue('opacity') === "1";
});
@ -62,7 +63,7 @@ casper.test.begin("Can't spam it", 2, function suite(test) {
casper.test.filename = "login_test.png";
casper.start(url + "ghost/signin/", function testTitle() {
casper.start(url + "ghost/login/", function testTitle() {
test.assertTitle("", "Ghost admin has no title");
}).viewport(1280, 1024);

View file

@ -1,6 +1,6 @@
/*globals casper, __utils__, url, testPost */
casper.test.begin("Ghost editor is correct", 8, function suite(test) {
casper.test.begin("Ghost editor is correct", 10, function suite(test) {
casper.test.filename = "editor_test.png";
@ -18,6 +18,12 @@ casper.test.begin("Ghost editor is correct", 8, function suite(test) {
}
}
// test saving with no data
casper.thenClick('.button-save').wait(500, function doneWait() {
test.assertExists('.notification-error', 'got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
});
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.content);
@ -30,11 +36,7 @@ casper.test.begin("Ghost editor is correct", 8, function suite(test) {
casper.on('resource.received', handleResource);
});
casper.thenClick('.button-save').wait(1000, function doneWait() {
this.echo("I've waited for another 1 seconds.");
});
casper.then(function checkPostWasCreated() {
casper.thenClick('.button-save').waitForResource(/posts/, function checkPostWasCreated() {
var urlRegExp = new RegExp("^" + url + "ghost\/editor\/[0-9]*");
test.assertUrlMatch(urlRegExp, 'got an id on our URL');
test.assertExists('.notification-success', 'got success notification');

View file

@ -113,4 +113,44 @@ casper.test.begin("Settings screen is correct", 19, function suite(test) {
casper.removeListener('resource.requested', handleSettingsRequest);
test.done();
});
});
casper.test.begin("User settings screen validates email", 6, function suite(test) {
var email, brokenEmail;
casper.test.filename = "user_settings_test.png";
casper.start(url + "ghost/settings/user", function testTitleAndUrl() {
test.assertTitle("", "Ghost admin has no title");
test.assertEquals(this.getCurrentUrl(), url + "ghost/settings/user", "Ghost doesn't require login this time");
}).viewport(1280, 1024);
casper.then(function setEmailToInvalid() {
email = casper.getElementInfo('#user-email').attributes.value;
brokenEmail = email.replace('.', '-');
casper.fillSelectors('.user-details-container', {
'#user-email': brokenEmail
}, false);
});
casper.thenClick('#user .button-save').waitForResource('/users/', function () {
test.assertExists('.notification-error', 'got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
});
casper.then(function resetEmailToValid() {
casper.fillSelectors('.user-details-container', {
'#user-email': email
}, false);
});
casper.thenClick('#user .button-save').waitForResource('/users/', function () {
test.assertExists('.notification-success', 'got success notification');
test.assertSelectorDoesntHaveText('.notification-success', '[object Object]');
});
casper.run(function () {
test.done();
});
});

View file

@ -0,0 +1,60 @@
/**
* Tests the flow of creating, editing and publishing tests
*/
/*globals casper, __utils__, url, testPost */
casper.test.begin("Ghost edit draft flow works correctly", 7, function suite(test) {
casper.test.filename = "flow_test.png";
casper.start(url + "ghost/editor", function then() {
test.assertEquals(casper.getCurrentUrl(), url + "ghost/editor", "Ghost doesn't require login this time");
}).viewport(1280, 1024);
// First, create a new draft post
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', 'Test Draft Post');
casper.writeContentToCodeMirror('I am a draft');
});
// We must wait after sending keys to CodeMirror
casper.wait(1000, function doneWait() {
this.echo("I've waited for 1 seconds.");
});
casper.thenClick('.button-save').waitForResource(/posts/, function then() {
test.assertExists('.notification-success', 'got success notification');
});
casper.thenOpen(url + 'ghost/content/', function then() {
test.assertEquals(casper.getCurrentUrl(), url + "ghost/content/", "Ghost doesn't require login this time");
});
casper.then(function then() {
test.assertEvalEquals(function () {
return document.querySelector('.content-list-content li').className;
}, "active", "first item is active");
test.assertSelectorHasText(".content-list-content li:first-child h3", 'Test Draft Post', "first item is the post we created");
});
casper.thenClick('.post-edit').waitForResource(/editor/, function then() {
test.assertUrlMatch(/editor/, "Ghost doesn't require login this time");
});
casper.thenClick('.button-save').waitForResource(/posts/, function then() {
test.assertExists('.notification-success', 'got success notification');
});
casper.run(function () {
test.done();
});
});
// TODO: test publishing, editing, republishing, unpublishing etc
//casper.test.begin("Ghost edit published flow works correctly", 6, function suite(test) {
//
// casper.test.filename = "flow_test.png";
//
//
//});

View file

@ -0,0 +1,98 @@
/**
* Tests logging out and attempting to sign up
*/
/*globals casper, __utils__, url, testPost, falseUser, email */
casper.test.begin("Ghost logout works correctly", 2, function suite(test) {
casper.test.filename = "logout_test.png";
casper.start(url + "ghost/", function then() {
test.assertEquals(casper.getCurrentUrl(), url + "ghost/", "Ghost doesn't require login this time");
}).viewport(1280, 1024);
casper.thenClick('#usermenu a').waitFor(function checkOpaque() {
return this.evaluate(function () {
var loginBox = document.querySelector('#usermenu .overlay.open');
return window.getComputedStyle(loginBox).getPropertyValue('display') === "block"
&& window.getComputedStyle(loginBox).getPropertyValue('opacity') === "1";
});
});
casper.thenClick('.usermenu-signout a').waitForResource(/login/, function then() {
test.assertExists('.notification-success', 'got success notification');
});
casper.run(function () {
test.done();
});
});
// has to be done after signing out
casper.test.begin("Can't spam signin", 3, function suite(test) {
casper.test.filename = "spam_test.png";
casper.start(url + "ghost/signin/", function testTitle() {
test.assertTitle("", "Ghost admin has no title");
}).viewport(1280, 1024);
casper.waitFor(function checkOpaque() {
return this.evaluate(function () {
var loginBox = document.querySelector('.login-box');
return window.getComputedStyle(loginBox).getPropertyValue('display') === "block"
&& window.getComputedStyle(loginBox).getPropertyValue('opacity') === "1";
});
}, function then() {
this.fill("#login", falseUser, true);
casper.wait(200, function doneWait() {
this.fill("#login", falseUser, true);
});
});
casper.wait(200, function doneWait() {
this.echo("I've waited for 1 seconds.");
});
casper.then(function testForErrorMessage() {
test.assertExists('.notification-error', 'got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
});
casper.run(function () {
test.done();
});
});
casper.test.begin("Ghost signup fails properly", 5, function suite(test) {
casper.test.filename = "signup_test.png";
casper.start(url + "ghost/signup/", function then() {
test.assertEquals(casper.getCurrentUrl(), url + "ghost/signup/", "Reached signup page");
}).viewport(1280, 1024);
casper.then(function signupWithShortPassword() {
this.fill("#register", {email: email, password: 'test'}, true);
});
// should now throw a short password error
casper.waitForResource(/signup/, function () {
test.assertExists('.notification-error', 'got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
});
casper.then(function signupWithLongPassword() {
this.fill("#register", {email: email, password: 'testing1234'}, true);
});
// should now throw a 1 user only error
casper.waitForResource(/signup/, function () {
test.assertExists('.notification-error', 'got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
});
casper.run(function () {
test.done();
});
});

View file

@ -3,7 +3,7 @@ var _ = require('underscore'),
should = require('should'),
helpers = require('./helpers'),
errors = require('../../server/errorHandling'),
Models = require('../../server/models');
Models = require('../../server/models'),
when = require('when');
describe('User Model', function run() {
@ -39,7 +39,7 @@ describe('User Model', function run() {
should.exist(createdUser);
createdUser.has('uuid').should.equal(true);
createdUser.attributes.password.should.not.equal(userData.password, "password was hashed");
createdUser.attributes.email_address.should.eql(userData.email_address, "email address corred");
createdUser.attributes.email_address.should.eql(userData.email_address, "email address correct");
done();
}).then(null, done);