2020-05-25 11:49:38 +03:00
|
|
|
|
const errors = require('@tryghost/errors');
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const should = require('should');
|
|
|
|
|
const sinon = require('sinon');
|
|
|
|
|
const testUtils = require('../../utils');
|
|
|
|
|
const moment = require('moment');
|
|
|
|
|
const _ = require('lodash');
|
|
|
|
|
const Promise = require('bluebird');
|
2020-08-11 20:44:21 +03:00
|
|
|
|
const {sequence} = require('@tryghost/promise');
|
2021-10-18 17:27:57 +03:00
|
|
|
|
const urlService = require('../../../core/server/services/url');
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const ghostBookshelf = require('../../../core/server/models/base');
|
|
|
|
|
const models = require('../../../core/server/models');
|
2021-03-18 20:16:37 +03:00
|
|
|
|
const db = require('../../../core/server/data/db');
|
2021-06-30 16:56:57 +03:00
|
|
|
|
const settingsCache = require('../../../core/shared/settings-cache');
|
2021-04-27 16:18:04 +03:00
|
|
|
|
const events = require('../../../core/server/lib/common/events');
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const configUtils = require('../../utils/configUtils');
|
|
|
|
|
const context = testUtils.context.owner;
|
|
|
|
|
const markdownToMobiledoc = testUtils.DataGenerator.markdownToMobiledoc;
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2017-01-25 16:47:49 +03:00
|
|
|
|
/**
|
|
|
|
|
* IMPORTANT:
|
|
|
|
|
* - do not spy the events unit, because when we only spy, all listeners get the event
|
|
|
|
|
* - this can cause unexpected behaviour as the listeners execute code
|
|
|
|
|
* - using rewire is not possible, because each model self registers it's model registry in bookshelf
|
|
|
|
|
* - rewire would add 1 registry, a file who requires the models, tries to register the model another time
|
|
|
|
|
*/
|
2013-10-08 05:39:33 +04:00
|
|
|
|
describe('Post Model', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let eventsTriggered = {};
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2020-02-24 23:51:09 +03:00
|
|
|
|
before(testUtils.teardownDb);
|
2019-01-22 19:54:50 +03:00
|
|
|
|
before(testUtils.stopGhost);
|
2020-02-24 23:51:09 +03:00
|
|
|
|
after(testUtils.teardownDb);
|
2018-04-25 18:13:35 +03:00
|
|
|
|
|
|
|
|
|
before(testUtils.setup('users:roles'));
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2016-06-02 20:11:54 +03:00
|
|
|
|
afterEach(function () {
|
2019-01-21 19:53:44 +03:00
|
|
|
|
sinon.restore();
|
2016-06-02 20:11:54 +03:00
|
|
|
|
});
|
|
|
|
|
|
✨Dynamic Routing Beta (#9596)
refs #9601
### Dynamic Routing
This is the beta version of dynamic routing.
- we had a initial implementation of "channels" available in the codebase
- we have removed and moved this implementation
- there is now a centralised place for dynamic routing - server/services/routing
- each routing component is represented by a router type e.g. collections, routes, static pages, taxonomies, rss, preview of posts
- keep as much as possible logic of routing helpers, middlewares and controllers
- ensure test coverage
- connect all the things together
- yaml file + validation
- routing + routers
- url service
- sitemaps
- url access
- deeper implementation of yaml validations
- e.g. hard require slashes
- ensure routing hierarchy/order
- e.g. you enable the subscriber app
- you have a custom static page, which lives under the same slug /subscribe
- static pages are stronger than apps
- e.g. the first collection owns the post it has filtered
- a post cannot live in two collections
- ensure apps are still working and hook into the routers layer (or better said: and register in the routing service)
- put as much as possible comments to the code base for better understanding
- ensure a clean debug log
- ensure we can unmount routes
- e.g. you have a collection permalink of /:slug/ represented by {globals.permalink}
- and you change the permalink in the admin to dated permalink
- the express route get's refreshed from /:slug/ to /:year/:month/:day/:slug/
- unmount without server restart, yey
- ensure we are backwards compatible
- e.g. render home.hbs for collection index if collection route is /
- ensure you can access your configured permalink from the settings table with {globals.permalink}
### Render 503 if url service did not finish
- return 503 if the url service has not finished generating the resource urls
### Rewrite sitemaps
- we have rewritten the sitemaps "service", because the url generator does no longer happen on runtime
- we generate all urls on bootstrap
- the sitemaps service will consume created resource and router urls
- these urls will be shown on the xml pages
- we listen on url events
- we listen on router events
- we no longer have to fetch the resources, which is nice
- the urlservice pre-fetches resources and emits their urls
- the urlservice is the only component who knows which urls are valid
- i made some ES6 adaptions
- we keep the caching logic -> only regenerate xml if there is a change
- updated tests
- checked test coverage (100%)
### Re-work usage of Url utility
- replace all usages of `urlService.utils.urlFor` by `urlService.getByResourceId`
- only for resources e.g. post, author, tag
- this is important, because with dynamic routing we no longer create static urls based on the settings permalink on runtime
- adapt url utility
- adapt tests
2018-06-05 20:02:20 +03:00
|
|
|
|
beforeEach(function () {
|
2019-01-21 19:53:44 +03:00
|
|
|
|
sinon.stub(urlService, 'getUrlByResourceId').withArgs(testUtils.DataGenerator.Content.posts[0].id).returns('/html-ipsum/');
|
✨Dynamic Routing Beta (#9596)
refs #9601
### Dynamic Routing
This is the beta version of dynamic routing.
- we had a initial implementation of "channels" available in the codebase
- we have removed and moved this implementation
- there is now a centralised place for dynamic routing - server/services/routing
- each routing component is represented by a router type e.g. collections, routes, static pages, taxonomies, rss, preview of posts
- keep as much as possible logic of routing helpers, middlewares and controllers
- ensure test coverage
- connect all the things together
- yaml file + validation
- routing + routers
- url service
- sitemaps
- url access
- deeper implementation of yaml validations
- e.g. hard require slashes
- ensure routing hierarchy/order
- e.g. you enable the subscriber app
- you have a custom static page, which lives under the same slug /subscribe
- static pages are stronger than apps
- e.g. the first collection owns the post it has filtered
- a post cannot live in two collections
- ensure apps are still working and hook into the routers layer (or better said: and register in the routing service)
- put as much as possible comments to the code base for better understanding
- ensure a clean debug log
- ensure we can unmount routes
- e.g. you have a collection permalink of /:slug/ represented by {globals.permalink}
- and you change the permalink in the admin to dated permalink
- the express route get's refreshed from /:slug/ to /:year/:month/:day/:slug/
- unmount without server restart, yey
- ensure we are backwards compatible
- e.g. render home.hbs for collection index if collection route is /
- ensure you can access your configured permalink from the settings table with {globals.permalink}
### Render 503 if url service did not finish
- return 503 if the url service has not finished generating the resource urls
### Rewrite sitemaps
- we have rewritten the sitemaps "service", because the url generator does no longer happen on runtime
- we generate all urls on bootstrap
- the sitemaps service will consume created resource and router urls
- these urls will be shown on the xml pages
- we listen on url events
- we listen on router events
- we no longer have to fetch the resources, which is nice
- the urlservice pre-fetches resources and emits their urls
- the urlservice is the only component who knows which urls are valid
- i made some ES6 adaptions
- we keep the caching logic -> only regenerate xml if there is a change
- updated tests
- checked test coverage (100%)
### Re-work usage of Url utility
- replace all usages of `urlService.utils.urlFor` by `urlService.getByResourceId`
- only for resources e.g. post, author, tag
- this is important, because with dynamic routing we no longer create static urls based on the settings permalink on runtime
- adapt url utility
- adapt tests
2018-06-05 20:02:20 +03:00
|
|
|
|
});
|
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
describe('Single author posts', function () {
|
|
|
|
|
afterEach(function () {
|
|
|
|
|
configUtils.restore();
|
|
|
|
|
});
|
2015-06-27 00:01:02 +03:00
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
describe('fetchOne/fetchAll/fetchPage', function () {
|
|
|
|
|
before(testUtils.fixtures.insertPostsAndTags);
|
|
|
|
|
after(function () {
|
|
|
|
|
return testUtils.truncate('posts_tags')
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('tags');
|
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts');
|
2020-09-17 11:46:45 +03:00
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts_meta');
|
2018-04-25 18:13:35 +03:00
|
|
|
|
});
|
2015-03-24 23:23:23 +03:00
|
|
|
|
});
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
|
|
2021-05-07 13:56:41 +03:00
|
|
|
|
describe('findOne', function () {
|
|
|
|
|
it('transforms legacy email_recipient_filter values on read', function (done) {
|
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[0].id;
|
|
|
|
|
|
|
|
|
|
db.knex('posts').where({id: postId}).update({
|
|
|
|
|
email_recipient_filter: 'paid'
|
|
|
|
|
}).then(() => {
|
|
|
|
|
return db.knex('posts').where({id: postId});
|
|
|
|
|
}).then((knexResult) => {
|
|
|
|
|
const [knexPost] = knexResult;
|
|
|
|
|
knexPost.email_recipient_filter.should.equal('paid');
|
|
|
|
|
|
|
|
|
|
return models.Post.findOne({id: postId});
|
|
|
|
|
}).then((result) => {
|
|
|
|
|
should.exist(result);
|
|
|
|
|
const post = result.toJSON();
|
|
|
|
|
post.email_recipient_filter.should.equal('status:-free');
|
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
describe('findPage', function () {
|
2021-05-24 12:51:38 +03:00
|
|
|
|
describe('with more posts/tags', function () {
|
2018-04-25 18:13:35 +03:00
|
|
|
|
beforeEach(function () {
|
|
|
|
|
return testUtils.truncate('posts_tags')
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('tags');
|
|
|
|
|
})
|
2021-05-24 12:51:38 +03:00
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts_meta');
|
|
|
|
|
})
|
2018-04-25 18:13:35 +03:00
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts');
|
|
|
|
|
});
|
|
|
|
|
});
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
beforeEach(function () {
|
|
|
|
|
return testUtils.fixtures.insertPostsAndTags()
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.fixtures.insertExtraPosts();
|
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.fixtures.insertExtraPostsTags();
|
|
|
|
|
});
|
|
|
|
|
});
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
it('can findPage, with various options', function (done) {
|
|
|
|
|
models.Post.findPage({page: 2})
|
|
|
|
|
.then(function (paginationResult) {
|
2019-08-19 14:41:09 +03:00
|
|
|
|
paginationResult.meta.pagination.page.should.equal(2);
|
|
|
|
|
paginationResult.meta.pagination.limit.should.equal(15);
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(4);
|
|
|
|
|
paginationResult.data.length.should.equal(15);
|
|
|
|
|
|
|
|
|
|
return models.Post.findPage({page: 5});
|
|
|
|
|
}).then(function (paginationResult) {
|
|
|
|
|
paginationResult.meta.pagination.page.should.equal(5);
|
|
|
|
|
paginationResult.meta.pagination.limit.should.equal(15);
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(4);
|
|
|
|
|
paginationResult.data.length.should.equal(0);
|
|
|
|
|
|
|
|
|
|
return models.Post.findPage({limit: 30});
|
|
|
|
|
}).then(function (paginationResult) {
|
|
|
|
|
paginationResult.meta.pagination.page.should.equal(1);
|
|
|
|
|
paginationResult.meta.pagination.limit.should.equal(30);
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(2);
|
|
|
|
|
paginationResult.data.length.should.equal(30);
|
|
|
|
|
|
|
|
|
|
// Test featured pages
|
|
|
|
|
return models.Post.findPage({limit: 10, filter: 'featured:true'});
|
|
|
|
|
}).then(function (paginationResult) {
|
|
|
|
|
paginationResult.meta.pagination.page.should.equal(1);
|
|
|
|
|
paginationResult.meta.pagination.limit.should.equal(10);
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(1);
|
|
|
|
|
paginationResult.data.length.should.equal(2);
|
|
|
|
|
|
|
|
|
|
// Test both boolean formats for featured pages
|
|
|
|
|
return models.Post.findPage({limit: 10, filter: 'featured:1'});
|
|
|
|
|
}).then(function (paginationResult) {
|
|
|
|
|
paginationResult.meta.pagination.page.should.equal(1);
|
|
|
|
|
paginationResult.meta.pagination.limit.should.equal(10);
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(1);
|
|
|
|
|
paginationResult.data.length.should.equal(2);
|
|
|
|
|
|
|
|
|
|
return models.Post.findPage({limit: 10, page: 2, status: 'all'});
|
|
|
|
|
}).then(function (paginationResult) {
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(11);
|
|
|
|
|
|
|
|
|
|
return models.Post.findPage({limit: 'all', status: 'all'});
|
|
|
|
|
}).then(function (paginationResult) {
|
|
|
|
|
paginationResult.meta.pagination.page.should.equal(1);
|
|
|
|
|
paginationResult.meta.pagination.limit.should.equal('all');
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(1);
|
|
|
|
|
paginationResult.data.length.should.equal(108);
|
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
2018-04-25 18:13:35 +03:00
|
|
|
|
});
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
it('can findPage for tag, with various options', function (done) {
|
|
|
|
|
// Test tag filter
|
|
|
|
|
models.Post.findPage({page: 1, filter: 'tags:bacon'})
|
|
|
|
|
.then(function (paginationResult) {
|
2019-08-19 14:41:09 +03:00
|
|
|
|
paginationResult.meta.pagination.page.should.equal(1);
|
|
|
|
|
paginationResult.meta.pagination.limit.should.equal(15);
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(1);
|
|
|
|
|
paginationResult.data.length.should.equal(2);
|
|
|
|
|
|
|
|
|
|
return models.Post.findPage({page: 1, filter: 'tags:kitchen-sink'});
|
|
|
|
|
}).then(function (paginationResult) {
|
|
|
|
|
paginationResult.meta.pagination.page.should.equal(1);
|
|
|
|
|
paginationResult.meta.pagination.limit.should.equal(15);
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(1);
|
|
|
|
|
paginationResult.data.length.should.equal(2);
|
|
|
|
|
|
|
|
|
|
return models.Post.findPage({page: 1, filter: 'tags:injection'});
|
|
|
|
|
}).then(function (paginationResult) {
|
|
|
|
|
paginationResult.meta.pagination.page.should.equal(1);
|
|
|
|
|
paginationResult.meta.pagination.limit.should.equal(15);
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(2);
|
|
|
|
|
paginationResult.data.length.should.equal(15);
|
|
|
|
|
|
|
|
|
|
return models.Post.findPage({page: 2, filter: 'tags:injection'});
|
|
|
|
|
}).then(function (paginationResult) {
|
|
|
|
|
paginationResult.meta.pagination.page.should.equal(2);
|
|
|
|
|
paginationResult.meta.pagination.limit.should.equal(15);
|
|
|
|
|
paginationResult.meta.pagination.pages.should.equal(2);
|
|
|
|
|
paginationResult.data.length.should.equal(10);
|
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
2018-04-25 18:13:35 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
2015-03-24 23:23:23 +03:00
|
|
|
|
});
|
2014-07-23 12:32:27 +04:00
|
|
|
|
});
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
describe('edit', function () {
|
2018-04-25 18:13:35 +03:00
|
|
|
|
beforeEach(testUtils.fixtures.insertPostsAndTags);
|
|
|
|
|
|
|
|
|
|
afterEach(function () {
|
|
|
|
|
return testUtils.truncate('posts_tags')
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('tags');
|
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts');
|
2020-09-17 11:46:45 +03:00
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts_meta');
|
2018-04-25 18:13:35 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2017-01-25 16:47:49 +03:00
|
|
|
|
beforeEach(function () {
|
|
|
|
|
eventsTriggered = {};
|
2017-02-03 16:15:11 +03:00
|
|
|
|
|
2020-05-25 11:49:38 +03:00
|
|
|
|
sinon.stub(events, 'emit').callsFake(function (eventName, eventObj) {
|
2017-01-25 16:47:49 +03:00
|
|
|
|
if (!eventsTriggered[eventName]) {
|
|
|
|
|
eventsTriggered[eventName] = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
eventsTriggered[eventName].push(eventObj);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2018-04-06 19:19:45 +03:00
|
|
|
|
it('[failure] multiple edits in one transaction', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const options = _.cloneDeep(context);
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
status: 'published'
|
|
|
|
|
};
|
2018-04-06 19:19:45 +03:00
|
|
|
|
|
|
|
|
|
return models.Base.transaction(function (txn) {
|
|
|
|
|
options.transacting = txn;
|
|
|
|
|
|
|
|
|
|
return models.Post.edit(data, _.merge({id: testUtils.DataGenerator.Content.posts[3].id}, options))
|
|
|
|
|
.then(function () {
|
|
|
|
|
return models.Post.edit(data, _.merge({id: testUtils.DataGenerator.Content.posts[5].id}, options));
|
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
// force rollback
|
|
|
|
|
throw new Error();
|
|
|
|
|
});
|
|
|
|
|
}).catch(function () {
|
|
|
|
|
// txn was rolled back
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('multiple edits in one transaction', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const options = _.cloneDeep(context);
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
status: 'published'
|
|
|
|
|
};
|
2018-04-06 19:19:45 +03:00
|
|
|
|
|
|
|
|
|
return models.Base.transaction(function (txn) {
|
|
|
|
|
options.transacting = txn;
|
|
|
|
|
|
|
|
|
|
return models.Post.edit(data, _.merge({id: testUtils.DataGenerator.Content.posts[3].id}, options))
|
|
|
|
|
.then(function () {
|
|
|
|
|
return models.Post.edit(data, _.merge({id: testUtils.DataGenerator.Content.posts[5].id}, options));
|
|
|
|
|
});
|
|
|
|
|
}).then(function () {
|
|
|
|
|
// txn was successful
|
2019-02-02 22:18:27 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(4);
|
2018-04-06 19:19:45 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2015-09-25 19:11:22 +03:00
|
|
|
|
it('can change title', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[0].id;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({id: postId}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(postId);
|
|
|
|
|
post.title.should.not.equal('new title');
|
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({title: 'new title'}, _.extend({}, context, {id: postId}));
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.title.should.equal('new title');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
|
|
|
|
should.exist(eventsTriggered['post.published.edited']);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
2014-12-10 17:03:39 +03:00
|
|
|
|
});
|
|
|
|
|
|
2017-08-01 11:39:34 +03:00
|
|
|
|
it('[failure] custom excerpt soft limit reached', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[0].id;
|
2017-08-01 11:39:34 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({id: postId}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2017-08-01 11:39:34 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(postId);
|
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({
|
2017-08-01 11:39:34 +03:00
|
|
|
|
custom_excerpt: new Array(302).join('a')
|
|
|
|
|
}, _.extend({}, context, {id: postId}));
|
|
|
|
|
}).then(function () {
|
|
|
|
|
done(new Error('expected validation error'));
|
|
|
|
|
}).catch(function (err) {
|
2021-12-01 13:22:01 +03:00
|
|
|
|
err[0].name.should.eql('ValidationError');
|
2017-08-01 11:39:34 +03:00
|
|
|
|
done();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2015-09-25 19:11:22 +03:00
|
|
|
|
it('can publish draft post', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[3].id;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({id: postId, status: 'draft'}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(postId);
|
|
|
|
|
post.status.should.equal('draft');
|
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({status: 'published'}, _.extend({}, context, {id: postId}));
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('published');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(4);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.published']);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['tag.attached']);
|
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2014-12-10 17:03:39 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
});
|
2014-12-10 17:03:39 +03:00
|
|
|
|
|
2015-09-25 19:11:22 +03:00
|
|
|
|
it('can unpublish published post', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[0].id;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({id: postId}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(postId);
|
|
|
|
|
post.status.should.equal('published');
|
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({status: 'draft'}, _.extend({}, context, {id: postId}));
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('draft');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(4);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.unpublished']);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
2014-12-10 17:03:39 +03:00
|
|
|
|
});
|
|
|
|
|
|
2016-04-14 14:22:38 +03:00
|
|
|
|
it('draft -> scheduled without published_at update', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2016-04-14 14:22:38 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({status: 'draft'}).then(function (results) {
|
2016-04-14 14:22:38 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.status.should.equal('draft');
|
|
|
|
|
|
2017-04-06 19:49:59 +03:00
|
|
|
|
results.set('published_at', null);
|
|
|
|
|
return results.save();
|
|
|
|
|
}).then(function () {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'scheduled'
|
|
|
|
|
}, _.extend({}, context, {id: post.id}));
|
2017-04-06 19:49:59 +03:00
|
|
|
|
}).then(function () {
|
|
|
|
|
done(new Error('expected error'));
|
2016-04-14 14:22:38 +03:00
|
|
|
|
}).catch(function (err) {
|
|
|
|
|
should.exist(err);
|
2020-05-25 11:49:38 +03:00
|
|
|
|
(err instanceof errors.ValidationError).should.eql(true);
|
2016-04-14 14:22:38 +03:00
|
|
|
|
done();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('draft -> scheduled: expect update of published_at', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const newPublishedAt = moment().add(1, 'day').toDate();
|
2016-04-14 14:22:38 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({status: 'draft'}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2016-04-14 14:22:38 +03:00
|
|
|
|
|
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.status.should.equal('draft');
|
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'scheduled',
|
|
|
|
|
published_at: newPublishedAt
|
|
|
|
|
}, _.extend({}, context, {id: post.id}));
|
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('scheduled');
|
|
|
|
|
|
|
|
|
|
// mysql does not store ms
|
|
|
|
|
moment(edited.attributes.published_at).startOf('seconds').diff(moment(newPublishedAt).startOf('seconds')).should.eql(0);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
|
|
|
|
should.exist(eventsTriggered['post.scheduled']);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
2016-04-14 14:22:38 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('scheduled -> draft: expect unschedule', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({status: 'scheduled'}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2016-04-14 14:22:38 +03:00
|
|
|
|
|
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.status.should.equal('scheduled');
|
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'draft'
|
|
|
|
|
}, _.extend({}, context, {id: post.id}));
|
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('draft');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
|
|
|
|
should.exist(eventsTriggered['post.unscheduled']);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
2016-04-14 14:22:38 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('scheduled -> scheduled with updated published_at', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({status: 'scheduled'}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2016-04-14 14:22:38 +03:00
|
|
|
|
|
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.status.should.equal('scheduled');
|
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'scheduled',
|
2017-04-06 19:49:59 +03:00
|
|
|
|
published_at: moment().add(20, 'days').toDate()
|
2016-04-14 14:22:38 +03:00
|
|
|
|
}, _.extend({}, context, {id: post.id}));
|
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('scheduled');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
|
|
|
|
should.exist(eventsTriggered['post.rescheduled']);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
2016-04-14 14:22:38 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
2016-05-19 14:49:22 +03:00
|
|
|
|
it('scheduled -> scheduled with unchanged published_at', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({status: 'scheduled'}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2016-05-19 14:49:22 +03:00
|
|
|
|
|
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.status.should.equal('scheduled');
|
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({
|
2016-05-19 14:49:22 +03:00
|
|
|
|
status: 'scheduled'
|
|
|
|
|
}, _.extend({}, context, {id: post.id}));
|
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('scheduled');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-02-02 22:18:27 +03:00
|
|
|
|
// nothing has changed
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(0);
|
2016-05-19 14:49:22 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
2017-04-06 19:49:59 +03:00
|
|
|
|
it('scheduled -> scheduled with unchanged published_at (within the 2 minutes window)', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2017-04-06 19:49:59 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({status: 'scheduled'}).then(function (results) {
|
2017-04-06 19:49:59 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.status.should.equal('scheduled');
|
|
|
|
|
|
|
|
|
|
results.set('published_at', moment().add(2, 'minutes').add(2, 'seconds').toDate());
|
|
|
|
|
return results.save();
|
|
|
|
|
}).then(function () {
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
|
|
|
|
should.exist(eventsTriggered['post.rescheduled']);
|
|
|
|
|
eventsTriggered = {};
|
|
|
|
|
|
|
|
|
|
return Promise.delay(1000 * 3);
|
|
|
|
|
}).then(function () {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({
|
2017-04-06 19:49:59 +03:00
|
|
|
|
status: 'scheduled'
|
|
|
|
|
}, _.extend({}, context, {id: post.id}));
|
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('scheduled');
|
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(1);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
2016-04-14 14:22:38 +03:00
|
|
|
|
it('published -> scheduled and expect update of published_at', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[0].id;
|
2016-04-14 14:22:38 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({id: postId}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2016-04-14 14:22:38 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(postId);
|
|
|
|
|
post.status.should.equal('published');
|
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'scheduled',
|
|
|
|
|
published_at: moment().add(1, 'day').toDate()
|
|
|
|
|
}, _.extend({}, context, {id: postId}));
|
2016-05-19 14:49:22 +03:00
|
|
|
|
}).then(function () {
|
|
|
|
|
done(new Error('change status from published to scheduled is not allowed right now!'));
|
|
|
|
|
}).catch(function (err) {
|
|
|
|
|
should.exist(err);
|
2020-05-25 11:49:38 +03:00
|
|
|
|
(err instanceof errors.ValidationError).should.eql(true);
|
2016-04-14 14:22:38 +03:00
|
|
|
|
done();
|
2016-05-19 14:49:22 +03:00
|
|
|
|
});
|
2016-04-14 14:22:38 +03:00
|
|
|
|
});
|
|
|
|
|
|
2015-09-25 19:11:22 +03:00
|
|
|
|
it('can convert draft post to page and back', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[3].id;
|
2014-12-10 17:03:39 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({id: postId, status: 'draft'}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(postId);
|
|
|
|
|
post.status.should.equal('draft');
|
|
|
|
|
|
2019-09-16 13:51:54 +03:00
|
|
|
|
return models.Post.edit({type: 'page'}, _.extend({}, context, {id: postId}));
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('draft');
|
2019-09-16 13:51:54 +03:00
|
|
|
|
edited.attributes.type.should.equal('page');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
|
|
|
|
should.exist(eventsTriggered['post.deleted']);
|
|
|
|
|
should.exist(eventsTriggered['page.added']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2019-09-16 13:51:54 +03:00
|
|
|
|
return models.Post.edit({type: 'post'}, _.extend({}, context, {id: postId}));
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('draft');
|
2019-09-16 13:51:54 +03:00
|
|
|
|
edited.attributes.type.should.equal('post');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(4);
|
|
|
|
|
should.exist(eventsTriggered['post.deleted']);
|
|
|
|
|
should.exist(eventsTriggered['page.added']);
|
|
|
|
|
should.exist(eventsTriggered['post.deleted']);
|
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
|
|
|
|
|
2014-12-10 17:03:39 +03:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
});
|
2014-12-10 17:03:39 +03:00
|
|
|
|
|
2016-04-14 14:22:38 +03:00
|
|
|
|
it('can convert draft to schedule AND post to page and back', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({status: 'draft'}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2016-04-14 14:22:38 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.status.should.equal('draft');
|
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({
|
2019-09-16 13:51:54 +03:00
|
|
|
|
type: 'page',
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'scheduled',
|
|
|
|
|
published_at: moment().add(10, 'days')
|
|
|
|
|
}, _.extend({}, context, {id: post.id}));
|
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('scheduled');
|
2019-09-16 13:51:54 +03:00
|
|
|
|
edited.attributes.type.should.equal('page');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(3);
|
|
|
|
|
should.exist(eventsTriggered['post.deleted']);
|
|
|
|
|
should.exist(eventsTriggered['page.added']);
|
|
|
|
|
should.exist(eventsTriggered['page.scheduled']);
|
2016-04-14 14:22:38 +03:00
|
|
|
|
|
2019-09-16 13:51:54 +03:00
|
|
|
|
return models.Post.edit({type: 'post'}, _.extend({}, context, {id: edited.id}));
|
2016-04-14 14:22:38 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('scheduled');
|
2019-09-16 13:51:54 +03:00
|
|
|
|
edited.attributes.type.should.equal('post');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(7);
|
|
|
|
|
should.exist(eventsTriggered['page.unscheduled']);
|
|
|
|
|
should.exist(eventsTriggered['page.deleted']);
|
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
|
|
|
|
should.exist(eventsTriggered['post.scheduled']);
|
|
|
|
|
|
2016-04-14 14:22:38 +03:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
2015-09-25 19:11:22 +03:00
|
|
|
|
it('can convert published post to page and back', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[0].id;
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({id: postId}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(postId);
|
|
|
|
|
post.status.should.equal('published');
|
|
|
|
|
|
2019-09-16 13:51:54 +03:00
|
|
|
|
return models.Post.edit({type: 'page'}, _.extend({}, context, {id: postId}));
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('published');
|
2019-09-16 13:51:54 +03:00
|
|
|
|
edited.attributes.type.should.equal('page');
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2017-01-25 16:47:49 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(4);
|
|
|
|
|
should.exist(eventsTriggered['post.unpublished']);
|
|
|
|
|
should.exist(eventsTriggered['post.deleted']);
|
|
|
|
|
should.exist(eventsTriggered['page.added']);
|
|
|
|
|
should.exist(eventsTriggered['page.published']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2019-09-16 13:51:54 +03:00
|
|
|
|
return models.Post.edit({type: 'post'}, _.extend({}, context, {id: postId}));
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('published');
|
2019-09-16 13:51:54 +03:00
|
|
|
|
edited.attributes.type.should.equal('post');
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2017-01-25 16:47:49 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(8);
|
|
|
|
|
should.exist(eventsTriggered['page.unpublished']);
|
|
|
|
|
should.exist(eventsTriggered['page.deleted']);
|
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
|
|
|
|
should.exist(eventsTriggered['post.published']);
|
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2014-07-23 12:32:27 +04:00
|
|
|
|
|
2015-09-25 19:11:22 +03:00
|
|
|
|
it('can change type and status at the same time', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[3].id;
|
2014-07-23 12:32:27 +04:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({id: postId, status: 'draft'}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(postId);
|
|
|
|
|
post.status.should.equal('draft');
|
|
|
|
|
|
2019-09-16 13:51:54 +03:00
|
|
|
|
return models.Post.edit({type: 'page', status: 'published'}, _.extend({}, context, {id: postId}));
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('published');
|
2019-09-16 13:51:54 +03:00
|
|
|
|
edited.attributes.type.should.equal('page');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(5);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.deleted']);
|
|
|
|
|
should.exist(eventsTriggered['page.added']);
|
|
|
|
|
should.exist(eventsTriggered['page.published']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['tag.attached']);
|
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2019-09-16 13:51:54 +03:00
|
|
|
|
return models.Post.edit({type: 'post', status: 'draft'}, _.extend({}, context, {id: postId}));
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('draft');
|
2019-09-16 13:51:54 +03:00
|
|
|
|
edited.attributes.type.should.equal('post');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(8);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['page.unpublished']);
|
|
|
|
|
should.exist(eventsTriggered['page.deleted']);
|
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2015-09-25 19:11:22 +03:00
|
|
|
|
|
|
|
|
|
it('cannot override the published_by setting', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[3].id;
|
2015-09-25 19:11:22 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne({id: postId, status: 'draft'}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2015-09-25 19:11:22 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(postId);
|
|
|
|
|
post.status.should.equal('draft');
|
|
|
|
|
|
|
|
|
|
// Test changing status and published_by at the same time
|
2018-04-06 19:19:45 +03:00
|
|
|
|
return models.Post.edit({
|
|
|
|
|
status: 'published',
|
|
|
|
|
published_by: 4
|
|
|
|
|
}, _.extend({}, context, {id: postId}));
|
2015-09-25 19:11:22 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('published');
|
|
|
|
|
edited.attributes.published_by.should.equal(context.context.user);
|
|
|
|
|
|
|
|
|
|
// Test changing status and published_by on its own
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit({published_by: 4}, _.extend({}, context, {id: postId}));
|
2015-09-25 19:11:22 +03:00
|
|
|
|
}).then(function (edited) {
|
|
|
|
|
should.exist(edited);
|
|
|
|
|
edited.attributes.status.should.equal('published');
|
|
|
|
|
edited.attributes.published_by.should.equal(context.context.user);
|
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2021-05-07 13:56:41 +03:00
|
|
|
|
|
|
|
|
|
it('transforms legacy email_recipient_filter values on save', function (done) {
|
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[3].id;
|
|
|
|
|
|
|
|
|
|
models.Post.findOne({id: postId}).then(() => {
|
|
|
|
|
return models.Post.edit({
|
|
|
|
|
email_recipient_filter: 'free'
|
|
|
|
|
}, _.extend({}, context, {id: postId}));
|
|
|
|
|
}).then((edited) => {
|
|
|
|
|
edited.attributes.email_recipient_filter.should.equal('status:free');
|
|
|
|
|
return db.knex('posts').where({id: edited.id});
|
|
|
|
|
}).then((knexResult) => {
|
|
|
|
|
const [knexPost] = knexResult;
|
|
|
|
|
knexPost.email_recipient_filter.should.equal('status:free');
|
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2021-05-10 21:32:11 +03:00
|
|
|
|
|
|
|
|
|
it('transforms special-case visibility values on save', function (done) {
|
|
|
|
|
// status:-free === paid
|
|
|
|
|
// status:-free,status:free (+variations) === members
|
|
|
|
|
|
|
|
|
|
const postId = testUtils.DataGenerator.Content.posts[3].id;
|
|
|
|
|
|
|
|
|
|
models.Post.findOne({id: postId}).then(() => {
|
|
|
|
|
return models.Post.edit({
|
|
|
|
|
visibility: 'status:-free'
|
|
|
|
|
}, _.extend({}, context, {id: postId}));
|
|
|
|
|
}).then((edited) => {
|
|
|
|
|
edited.attributes.visibility.should.equal('paid');
|
|
|
|
|
return db.knex('posts').where({id: edited.id});
|
|
|
|
|
}).then((knexResult) => {
|
|
|
|
|
const [knexPost] = knexResult;
|
|
|
|
|
knexPost.visibility.should.equal('paid');
|
|
|
|
|
}).then(() => {
|
|
|
|
|
return models.Post.edit({
|
|
|
|
|
visibility: 'status:-free,status:free'
|
|
|
|
|
}, _.extend({}, context, {id: postId}));
|
|
|
|
|
}).then((edited) => {
|
|
|
|
|
edited.attributes.visibility.should.equal('members');
|
|
|
|
|
|
|
|
|
|
return models.Post.edit({
|
|
|
|
|
visibility: 'status:free,status:-free'
|
|
|
|
|
}, _.extend({}, context, {id: postId}));
|
|
|
|
|
}).then((edited) => {
|
|
|
|
|
edited.attributes.visibility.should.equal('members');
|
|
|
|
|
|
|
|
|
|
return models.Post.edit({
|
|
|
|
|
visibility: 'status:free,status:-free,label:vip'
|
|
|
|
|
}, _.extend({}, context, {id: postId}));
|
|
|
|
|
}).then((edited) => {
|
|
|
|
|
edited.attributes.visibility.should.equal('members');
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2014-07-23 12:32:27 +04:00
|
|
|
|
});
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
describe('add', function () {
|
2018-04-25 18:13:35 +03:00
|
|
|
|
before(testUtils.fixtures.insertPostsAndTags);
|
|
|
|
|
|
|
|
|
|
after(function () {
|
|
|
|
|
return testUtils.truncate('posts_tags')
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('tags');
|
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts');
|
2020-09-17 11:46:45 +03:00
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts_meta');
|
2018-04-25 18:13:35 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2017-01-25 16:47:49 +03:00
|
|
|
|
beforeEach(function () {
|
|
|
|
|
eventsTriggered = {};
|
2017-02-03 16:15:11 +03:00
|
|
|
|
|
2020-05-25 11:49:38 +03:00
|
|
|
|
sinon.stub(events, 'emit').callsFake(function (eventName, eventObj) {
|
2017-01-25 16:47:49 +03:00
|
|
|
|
if (!eventsTriggered[eventName]) {
|
|
|
|
|
eventsTriggered[eventName] = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
eventsTriggered[eventName].push(eventObj);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
it('can add, defaults are all correct', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let createdPostUpdatedDate;
|
|
|
|
|
const newPost = testUtils.DataGenerator.forModel.posts[2];
|
|
|
|
|
const newPostDB = testUtils.DataGenerator.Content.posts[2];
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add(newPost, _.merge({withRelated: ['author']}, context)).then(function (createdPost) {
|
|
|
|
|
return models.Post.findOne({id: createdPost.id, status: 'all'});
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (createdPost) {
|
|
|
|
|
should.exist(createdPost);
|
|
|
|
|
createdPost.has('uuid').should.equal(true);
|
|
|
|
|
createdPost.get('status').should.equal('draft');
|
|
|
|
|
createdPost.get('title').should.equal(newPost.title, 'title is correct');
|
2017-05-31 17:46:29 +03:00
|
|
|
|
createdPost.get('mobiledoc').should.equal(newPost.mobiledoc, 'mobiledoc is correct');
|
2015-03-24 23:23:23 +03:00
|
|
|
|
createdPost.has('html').should.equal(true);
|
|
|
|
|
createdPost.get('html').should.equal(newPostDB.html);
|
2017-04-11 11:55:36 +03:00
|
|
|
|
createdPost.has('plaintext').should.equal(true);
|
|
|
|
|
createdPost.get('plaintext').should.match(/^testing/);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
createdPost.get('slug').should.equal(newPostDB.slug + '-2');
|
|
|
|
|
(!!createdPost.get('featured')).should.equal(false);
|
|
|
|
|
(!!createdPost.get('page')).should.equal(false);
|
2017-05-31 18:05:49 +03:00
|
|
|
|
|
|
|
|
|
should.equal(createdPost.get('locale'), null);
|
2019-09-26 16:40:24 +03:00
|
|
|
|
should.equal(createdPost.get('visibility'), 'public');
|
|
|
|
|
|
|
|
|
|
// testing for nulls
|
|
|
|
|
(createdPost.get('feature_image') === null).should.equal(true);
|
|
|
|
|
|
|
|
|
|
createdPost.get('created_at').should.be.above(new Date(0).getTime());
|
|
|
|
|
createdPost.get('created_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
|
|
|
|
createdPost.get('author_id').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
|
|
|
|
createdPost.has('author').should.equal(false);
|
|
|
|
|
createdPost.get('created_by').should.equal(createdPost.get('author_id'));
|
|
|
|
|
createdPost.get('updated_at').should.be.above(new Date(0).getTime());
|
|
|
|
|
createdPost.get('updated_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
|
|
|
|
should.equal(createdPost.get('published_at'), null);
|
|
|
|
|
should.equal(createdPost.get('published_by'), null);
|
|
|
|
|
|
|
|
|
|
createdPostUpdatedDate = createdPost.get('updated_at');
|
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
|
|
|
|
|
|
|
|
|
// Set the status to published to check that `published_at` is set.
|
|
|
|
|
return createdPost.save({status: 'published'}, context);
|
|
|
|
|
}).then(function (publishedPost) {
|
|
|
|
|
publishedPost.get('published_at').should.be.instanceOf(Date);
|
|
|
|
|
publishedPost.get('published_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
|
|
|
|
publishedPost.get('updated_at').should.be.instanceOf(Date);
|
|
|
|
|
publishedPost.get('updated_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
|
|
|
|
publishedPost.get('updated_at').should.not.equal(createdPostUpdatedDate);
|
|
|
|
|
|
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(4);
|
|
|
|
|
should.exist(eventsTriggered['post.published']);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('can add, default visibility is taken from settings cache', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const originalSettingsCacheGetFn = settingsCache.get;
|
2019-10-04 12:17:00 +03:00
|
|
|
|
sinon.stub(settingsCache, 'get')
|
|
|
|
|
.callsFake(function (key, options) {
|
|
|
|
|
if (key === 'labs') {
|
|
|
|
|
return {
|
|
|
|
|
members: true
|
|
|
|
|
};
|
|
|
|
|
} else if (key === 'default_content_visibility') {
|
|
|
|
|
return 'paid';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return originalSettingsCacheGetFn(key, options);
|
|
|
|
|
});
|
2019-09-26 16:40:24 +03:00
|
|
|
|
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let createdPostUpdatedDate;
|
|
|
|
|
const newPost = testUtils.DataGenerator.forModel.posts[2];
|
|
|
|
|
const newPostDB = testUtils.DataGenerator.Content.posts[2];
|
2019-09-26 16:40:24 +03:00
|
|
|
|
|
|
|
|
|
models.Post.add(newPost, _.merge({withRelated: ['author']}, context)).then(function (createdPost) {
|
|
|
|
|
return models.Post.findOne({id: createdPost.id, status: 'all'});
|
|
|
|
|
}).then(function (createdPost) {
|
|
|
|
|
should.exist(createdPost);
|
|
|
|
|
createdPost.has('uuid').should.equal(true);
|
|
|
|
|
createdPost.get('status').should.equal('draft');
|
|
|
|
|
createdPost.get('title').should.equal(newPost.title, 'title is correct');
|
|
|
|
|
createdPost.get('mobiledoc').should.equal(newPost.mobiledoc, 'mobiledoc is correct');
|
|
|
|
|
createdPost.has('html').should.equal(true);
|
|
|
|
|
createdPost.get('html').should.equal(newPostDB.html);
|
|
|
|
|
createdPost.has('plaintext').should.equal(true);
|
|
|
|
|
createdPost.get('plaintext').should.match(/^testing/);
|
2019-10-04 12:17:00 +03:00
|
|
|
|
// createdPost.get('slug').should.equal(newPostDB.slug + '-3');
|
2019-09-26 16:40:24 +03:00
|
|
|
|
(!!createdPost.get('featured')).should.equal(false);
|
|
|
|
|
(!!createdPost.get('page')).should.equal(false);
|
|
|
|
|
|
|
|
|
|
should.equal(createdPost.get('locale'), null);
|
|
|
|
|
should.equal(createdPost.get('visibility'), 'paid');
|
2017-05-31 18:05:49 +03:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
// testing for nulls
|
2017-04-24 20:21:47 +03:00
|
|
|
|
(createdPost.get('feature_image') === null).should.equal(true);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
|
|
|
|
createdPost.get('created_at').should.be.above(new Date(0).getTime());
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
|
createdPost.get('created_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
|
|
|
|
createdPost.get('author_id').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
createdPost.has('author').should.equal(false);
|
|
|
|
|
createdPost.get('created_by').should.equal(createdPost.get('author_id'));
|
|
|
|
|
createdPost.get('updated_at').should.be.above(new Date(0).getTime());
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
|
createdPost.get('updated_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.equal(createdPost.get('published_at'), null);
|
|
|
|
|
should.equal(createdPost.get('published_by'), null);
|
|
|
|
|
|
|
|
|
|
createdPostUpdatedDate = createdPost.get('updated_at');
|
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
|
|
|
|
// Set the status to published to check that `published_at` is set.
|
|
|
|
|
return createdPost.save({status: 'published'}, context);
|
|
|
|
|
}).then(function (publishedPost) {
|
|
|
|
|
publishedPost.get('published_at').should.be.instanceOf(Date);
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
|
publishedPost.get('published_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
publishedPost.get('updated_at').should.be.instanceOf(Date);
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
|
publishedPost.get('updated_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
publishedPost.get('updated_at').should.not.equal(createdPostUpdatedDate);
|
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(4);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.published']);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
it('can add, with previous published_at date', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const previousPublishedAtDate = new Date(2013, 8, 21, 12);
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add({
|
2015-03-24 23:23:23 +03:00
|
|
|
|
status: 'published',
|
|
|
|
|
published_at: previousPublishedAtDate,
|
|
|
|
|
title: 'published_at test',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('This is some content')
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}, context).then(function (newPost) {
|
|
|
|
|
should.exist(newPost);
|
|
|
|
|
new Date(newPost.get('published_at')).getTime().should.equal(previousPublishedAtDate.getTime());
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(3);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
|
|
|
|
should.exist(eventsTriggered['post.published']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2013-12-20 02:51:28 +04:00
|
|
|
|
|
2016-04-14 14:22:38 +03:00
|
|
|
|
it('add draft post without published_at -> we expect no auto insert of published_at', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'draft',
|
|
|
|
|
title: 'draft 1',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('This is some content')
|
2016-04-14 14:22:38 +03:00
|
|
|
|
}, context).then(function (newPost) {
|
|
|
|
|
should.exist(newPost);
|
|
|
|
|
should.not.exist(newPost.get('published_at'));
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2016-04-14 14:22:38 +03:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
2018-03-27 17:16:15 +03:00
|
|
|
|
it('add multiple authors', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add({
|
2018-03-27 17:16:15 +03:00
|
|
|
|
status: 'draft',
|
|
|
|
|
title: 'draft 1',
|
|
|
|
|
mobiledoc: markdownToMobiledoc('This is some content'),
|
|
|
|
|
authors: [{
|
|
|
|
|
id: testUtils.DataGenerator.forKnex.users[0].id,
|
|
|
|
|
name: testUtils.DataGenerator.forKnex.users[0].name
|
|
|
|
|
}]
|
|
|
|
|
}, _.merge({withRelated: ['authors']}, context)).then(function (newPost) {
|
|
|
|
|
should.exist(newPost);
|
|
|
|
|
newPost.toJSON().author.should.eql(testUtils.DataGenerator.forKnex.users[0].id);
|
|
|
|
|
newPost.toJSON().authors.length.should.eql(1);
|
|
|
|
|
newPost.toJSON().authors[0].id.should.eql(testUtils.DataGenerator.forKnex.users[0].id);
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
2016-04-14 14:22:38 +03:00
|
|
|
|
it('add draft post with published_at -> we expect published_at to exist', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'draft',
|
|
|
|
|
published_at: moment().toDate(),
|
|
|
|
|
title: 'draft 1',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('This is some content')
|
2016-04-14 14:22:38 +03:00
|
|
|
|
}, context).then(function (newPost) {
|
|
|
|
|
should.exist(newPost);
|
|
|
|
|
should.exist(newPost.get('published_at'));
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2016-04-14 14:22:38 +03:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('add scheduled post without published_at -> we expect an error', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'scheduled',
|
|
|
|
|
title: 'scheduled 1',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('This is some content')
|
2016-04-14 14:22:38 +03:00
|
|
|
|
}, context).catch(function (err) {
|
|
|
|
|
should.exist(err);
|
2020-05-25 11:49:38 +03:00
|
|
|
|
(err instanceof errors.ValidationError).should.eql(true);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(0);
|
2016-04-14 14:22:38 +03:00
|
|
|
|
done();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('add scheduled post with published_at not in future-> we expect an error', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'scheduled',
|
|
|
|
|
published_at: moment().subtract(1, 'minute'),
|
|
|
|
|
title: 'scheduled 1',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('This is some content')
|
2016-04-14 14:22:38 +03:00
|
|
|
|
}, context).catch(function (err) {
|
|
|
|
|
should.exist(err);
|
2020-05-25 11:49:38 +03:00
|
|
|
|
(err instanceof errors.ValidationError).should.eql(true);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(0);
|
2016-04-14 14:22:38 +03:00
|
|
|
|
done();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('add scheduled post with published_at 1 minutes in future -> we expect an error', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'scheduled',
|
|
|
|
|
published_at: moment().add(1, 'minute'),
|
|
|
|
|
title: 'scheduled 1',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('This is some content')
|
2016-04-14 14:22:38 +03:00
|
|
|
|
}, context).catch(function (err) {
|
2020-05-25 11:49:38 +03:00
|
|
|
|
(err instanceof errors.ValidationError).should.eql(true);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(0);
|
2016-04-14 14:22:38 +03:00
|
|
|
|
done();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('add scheduled post with published_at 10 minutes in future -> we expect success', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add({
|
2016-04-14 14:22:38 +03:00
|
|
|
|
status: 'scheduled',
|
|
|
|
|
published_at: moment().add(10, 'minute'),
|
|
|
|
|
title: 'scheduled 1',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('This is some content')
|
2016-04-14 14:22:38 +03:00
|
|
|
|
}, context).then(function (post) {
|
|
|
|
|
should.exist(post);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(3);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
|
|
|
|
should.exist(eventsTriggered['post.scheduled']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2016-04-14 14:22:38 +03:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
it('can generate a non conflicting slug', function (done) {
|
|
|
|
|
// Create 12 posts with the same title
|
|
|
|
|
sequence(_.times(12, function (i) {
|
|
|
|
|
return function () {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.add({
|
2015-03-24 23:23:23 +03:00
|
|
|
|
title: 'Test Title',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('Test Content ' + (i + 1))
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}, context);
|
|
|
|
|
};
|
|
|
|
|
})).then(function (createdPosts) {
|
|
|
|
|
// Should have created 12 posts
|
|
|
|
|
createdPosts.length.should.equal(12);
|
|
|
|
|
|
|
|
|
|
// Should have unique slugs and contents
|
|
|
|
|
_(createdPosts).each(function (post, i) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const num = i + 1;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
|
|
|
|
// First one has normal title
|
|
|
|
|
if (num === 1) {
|
|
|
|
|
post.get('slug').should.equal('test-title');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
post.get('slug').should.equal('test-title-' + num);
|
2017-05-31 17:46:29 +03:00
|
|
|
|
JSON.parse(post.get('mobiledoc')).cards[0][1].markdown.should.equal('Test Content ' + num);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
eventsTriggered['post.added'].length.should.eql(12);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('can generate slugs without duplicate hyphens', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const newPost = {
|
2015-03-24 23:23:23 +03:00
|
|
|
|
title: 'apprehensive titles have too many spaces—and m-dashes — – and also n-dashes ',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('Test Content 1')
|
2014-07-23 12:32:27 +04:00
|
|
|
|
};
|
2013-12-20 02:51:28 +04:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add(newPost, context).then(function (createdPost) {
|
2015-03-24 23:23:23 +03:00
|
|
|
|
createdPost.get('slug').should.equal('apprehensive-titles-have-too-many-spaces-and-m-dashes-and-also-n-dashes');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2013-12-20 02:51:28 +04:00
|
|
|
|
|
2019-11-18 17:25:33 +03:00
|
|
|
|
it('can generate a safe slug when a protected keyword is used', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const newPost = {
|
2015-03-24 23:23:23 +03:00
|
|
|
|
title: 'rss',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('Test Content 1')
|
2015-03-24 23:23:23 +03:00
|
|
|
|
};
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add(newPost, context).then(function (createdPost) {
|
2015-03-24 23:23:23 +03:00
|
|
|
|
createdPost.get('slug').should.not.equal('rss');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
done();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('can generate slugs without non-ascii characters', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const newPost = {
|
2015-03-24 23:23:23 +03:00
|
|
|
|
title: 'भुते धडकी भरवणारा आहेत',
|
2017-05-31 17:46:29 +03:00
|
|
|
|
mobiledoc: markdownToMobiledoc('Test Content 1')
|
2015-03-24 23:23:23 +03:00
|
|
|
|
};
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add(newPost, context).then(function (createdPost) {
|
2015-03-24 23:23:23 +03:00
|
|
|
|
createdPost.get('slug').should.equal('bhute-dhddkii-bhrvnnaaraa-aahet');
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2014-07-23 12:32:27 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
it('detects duplicate slugs before saving', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const firstPost = {
|
|
|
|
|
title: 'First post',
|
|
|
|
|
mobiledoc: markdownToMobiledoc('First content 1')
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const secondPost = {
|
|
|
|
|
title: 'Second post',
|
|
|
|
|
mobiledoc: markdownToMobiledoc('Second content 1')
|
|
|
|
|
};
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
|
|
|
|
// Create the first post
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.add(firstPost, context)
|
2015-03-24 23:23:23 +03:00
|
|
|
|
.then(function (createdFirstPost) {
|
|
|
|
|
// Store the slug for later
|
|
|
|
|
firstPost.slug = createdFirstPost.get('slug');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
|
|
|
|
// Create the second post
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.add(secondPost, context);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (createdSecondPost) {
|
2017-03-21 11:24:11 +03:00
|
|
|
|
// Store the slug for comparison later
|
2019-08-19 14:41:09 +03:00
|
|
|
|
secondPost.slug = createdSecondPost.get('slug');
|
2017-01-25 16:47:49 +03:00
|
|
|
|
|
2019-08-19 14:41:09 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
|
|
|
|
should.exist(eventsTriggered['post.added']);
|
|
|
|
|
should.exist(eventsTriggered['user.attached']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2019-08-19 14:41:09 +03:00
|
|
|
|
// Update with a conflicting slug from the first post
|
|
|
|
|
return createdSecondPost.save({
|
|
|
|
|
slug: firstPost.slug
|
|
|
|
|
}, context);
|
|
|
|
|
}).then(function (updatedSecondPost) {
|
2017-03-21 11:24:11 +03:00
|
|
|
|
// Should have updated from original
|
2019-08-19 14:41:09 +03:00
|
|
|
|
updatedSecondPost.get('slug').should.not.equal(secondPost.slug);
|
|
|
|
|
// Should not have a conflicted slug from the first
|
|
|
|
|
updatedSecondPost.get('slug').should.not.equal(firstPost.slug);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2019-08-19 14:41:09 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(3);
|
|
|
|
|
should.exist(eventsTriggered['post.edited']);
|
2017-03-21 11:24:11 +03:00
|
|
|
|
|
2019-08-19 14:41:09 +03:00
|
|
|
|
return models.Post.findOne({
|
|
|
|
|
id: updatedSecondPost.id,
|
|
|
|
|
status: 'all'
|
|
|
|
|
});
|
|
|
|
|
}).then(function (foundPost) {
|
2017-03-21 11:24:11 +03:00
|
|
|
|
// Should have updated from original
|
2019-08-19 14:41:09 +03:00
|
|
|
|
foundPost.get('slug').should.not.equal(secondPost.slug);
|
|
|
|
|
// Should not have a conflicted slug from the first
|
|
|
|
|
foundPost.get('slug').should.not.equal(firstPost.slug);
|
2017-03-21 11:24:11 +03:00
|
|
|
|
|
2019-08-19 14:41:09 +03:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
2013-10-08 05:39:33 +04:00
|
|
|
|
});
|
2019-10-07 22:23:45 +03:00
|
|
|
|
|
2021-03-23 12:11:24 +03:00
|
|
|
|
it('it stores urls as transform-ready and reads as absolute', function (done) {
|
2019-10-07 22:23:45 +03:00
|
|
|
|
const post = {
|
2021-03-05 16:54:01 +03:00
|
|
|
|
title: 'Absolute->Transform-ready URL Transform Test',
|
2019-10-07 22:23:45 +03:00
|
|
|
|
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"http://127.0.0.1:2369/content/images/card.jpg"}]],"markups":[["a",["href","http://127.0.0.1:2369/test"]]],"sections":[[1,"p",[[0,[0],1,"Testing"]]],[10,0]]}',
|
|
|
|
|
custom_excerpt: 'Testing <a href="http://127.0.0.1:2369/internal">links</a> in custom excerpts',
|
|
|
|
|
codeinjection_head: '<script src="http://127.0.0.1:2369/assets/head.js"></script>',
|
|
|
|
|
codeinjection_foot: '<script src="http://127.0.0.1:2369/assets/foot.js"></script>',
|
|
|
|
|
feature_image: 'http://127.0.0.1:2369/content/images/feature.png',
|
2019-10-09 15:38:08 +03:00
|
|
|
|
canonical_url: 'http://127.0.0.1:2369/canonical',
|
|
|
|
|
posts_meta: {
|
|
|
|
|
og_image: 'http://127.0.0.1:2369/content/images/og.png',
|
|
|
|
|
twitter_image: 'http://127.0.0.1:2369/content/images/twitter.png'
|
|
|
|
|
}
|
2019-10-07 22:23:45 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
models.Post.add(post, context).then((createdPost) => {
|
2021-03-18 20:16:37 +03:00
|
|
|
|
createdPost.get('mobiledoc').should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"http://127.0.0.1:2369/content/images/card.jpg"}]],"markups":[["a",["href","http://127.0.0.1:2369/test"]]],"sections":[[1,"p",[[0,[0],1,"Testing"]]],[10,0]]}');
|
|
|
|
|
createdPost.get('html').should.equal('<p><a href="http://127.0.0.1:2369/test">Testing</a></p><figure class="kg-card kg-image-card"><img src="http://127.0.0.1:2369/content/images/card.jpg" class="kg-image" alt loading="lazy"></figure>');
|
2021-03-23 19:56:14 +03:00
|
|
|
|
createdPost.get('plaintext').should.containEql('Testing [http://127.0.0.1:2369/test]');
|
2021-03-18 20:16:37 +03:00
|
|
|
|
createdPost.get('custom_excerpt').should.equal('Testing <a href="http://127.0.0.1:2369/internal">links</a> in custom excerpts');
|
|
|
|
|
createdPost.get('codeinjection_head').should.equal('<script src="http://127.0.0.1:2369/assets/head.js"></script>');
|
|
|
|
|
createdPost.get('codeinjection_foot').should.equal('<script src="http://127.0.0.1:2369/assets/foot.js"></script>');
|
|
|
|
|
createdPost.get('feature_image').should.equal('http://127.0.0.1:2369/content/images/feature.png');
|
|
|
|
|
createdPost.get('canonical_url').should.equal('http://127.0.0.1:2369/canonical');
|
2019-10-07 22:23:45 +03:00
|
|
|
|
|
2019-10-09 15:38:08 +03:00
|
|
|
|
const postMeta = createdPost.relations.posts_meta;
|
|
|
|
|
|
2021-03-18 20:16:37 +03:00
|
|
|
|
postMeta.get('og_image').should.equal('http://127.0.0.1:2369/content/images/og.png');
|
|
|
|
|
postMeta.get('twitter_image').should.equal('http://127.0.0.1:2369/content/images/twitter.png');
|
2019-10-09 15:38:08 +03:00
|
|
|
|
|
2019-10-07 22:23:45 +03:00
|
|
|
|
// ensure canonical_url is not transformed when protocol does not match
|
|
|
|
|
return createdPost.save({
|
|
|
|
|
canonical_url: 'https://127.0.0.1:2369/https-internal',
|
|
|
|
|
// sanity check for general absolute->relative transform during edits
|
|
|
|
|
feature_image: 'http://127.0.0.1:2369/content/images/updated_feature.png'
|
|
|
|
|
});
|
|
|
|
|
}).then((updatedPost) => {
|
|
|
|
|
updatedPost.get('canonical_url').should.equal('https://127.0.0.1:2369/https-internal');
|
2021-03-18 20:16:37 +03:00
|
|
|
|
updatedPost.get('feature_image').should.equal('http://127.0.0.1:2369/content/images/updated_feature.png');
|
|
|
|
|
|
|
|
|
|
return updatedPost;
|
|
|
|
|
}).then((updatedPost) => {
|
|
|
|
|
return db.knex('posts').where({id: updatedPost.id});
|
|
|
|
|
}).then((knexResult) => {
|
|
|
|
|
const [knexPost] = knexResult;
|
|
|
|
|
knexPost.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"__GHOST_URL__/content/images/card.jpg"}]],"markups":[["a",["href","__GHOST_URL__/test"]]],"sections":[[1,"p",[[0,[0],1,"Testing"]]],[10,0]]}');
|
|
|
|
|
knexPost.html.should.equal('<p><a href="__GHOST_URL__/test">Testing</a></p><figure class="kg-card kg-image-card"><img src="__GHOST_URL__/content/images/card.jpg" class="kg-image" alt loading="lazy"></figure>');
|
2021-03-23 19:56:14 +03:00
|
|
|
|
knexPost.plaintext.should.containEql('Testing [__GHOST_URL__/test]');
|
2021-03-18 20:16:37 +03:00
|
|
|
|
knexPost.custom_excerpt.should.equal('Testing <a href="__GHOST_URL__/internal">links</a> in custom excerpts');
|
|
|
|
|
knexPost.codeinjection_head.should.equal('<script src="__GHOST_URL__/assets/head.js"></script>');
|
|
|
|
|
knexPost.codeinjection_foot.should.equal('<script src="__GHOST_URL__/assets/foot.js"></script>');
|
|
|
|
|
knexPost.feature_image.should.equal('__GHOST_URL__/content/images/updated_feature.png');
|
|
|
|
|
knexPost.canonical_url.should.equal('https://127.0.0.1:2369/https-internal');
|
2019-10-07 22:23:45 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2014-07-23 12:32:27 +04:00
|
|
|
|
});
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
describe('destroy', function () {
|
2018-04-25 18:13:35 +03:00
|
|
|
|
beforeEach(testUtils.fixtures.insertPostsAndTags);
|
|
|
|
|
|
|
|
|
|
afterEach(function () {
|
|
|
|
|
return testUtils.truncate('posts_tags')
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('tags');
|
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts');
|
2020-09-17 11:46:45 +03:00
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts_meta');
|
2018-04-25 18:13:35 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2017-01-25 16:47:49 +03:00
|
|
|
|
beforeEach(function () {
|
|
|
|
|
eventsTriggered = {};
|
2020-05-25 11:49:38 +03:00
|
|
|
|
sinon.stub(events, 'emit').callsFake(function (eventName, eventObj) {
|
2017-01-25 16:47:49 +03:00
|
|
|
|
if (!eventsTriggered[eventName]) {
|
|
|
|
|
eventsTriggered[eventName] = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
eventsTriggered[eventName].push(eventObj);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
it('published post', function (done) {
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
|
// We're going to try deleting post id 1 which has tag id 1
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const firstItemData = {id: testUtils.DataGenerator.Content.posts[0].id};
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
// Test that we have the post we expect, with exactly one tag
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne(firstItemData, {withRelated: ['tags']}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(firstItemData.id);
|
|
|
|
|
post.status.should.equal('published');
|
|
|
|
|
post.tags.should.have.length(2);
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
|
post.tags[0].id.should.equal(testUtils.DataGenerator.Content.tags[0].id);
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
// Destroy the post
|
2016-03-02 07:42:01 +03:00
|
|
|
|
return results.destroy();
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (response) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const deleted = response.toJSON();
|
2014-07-23 12:32:27 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.equal(deleted.author, undefined);
|
|
|
|
|
|
2019-02-03 15:36:08 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(5);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.unpublished']);
|
|
|
|
|
should.exist(eventsTriggered['post.deleted']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.detached']);
|
|
|
|
|
should.exist(eventsTriggered['tag.detached']);
|
2019-02-03 15:36:08 +03:00
|
|
|
|
should.exist(eventsTriggered['post.tag.detached']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
|
|
|
|
// Double check we can't find the post again
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.findOne(firstItemData);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (newResults) {
|
|
|
|
|
should.equal(newResults, null);
|
|
|
|
|
|
|
|
|
|
// Double check we can't find any related tags
|
|
|
|
|
return ghostBookshelf.knex.select().table('posts_tags').where('post_id', firstItemData.id);
|
|
|
|
|
}).then(function (postsTags) {
|
2016-02-08 00:27:01 +03:00
|
|
|
|
postsTags.should.be.empty();
|
2014-07-23 12:32:27 +04:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
});
|
2013-10-08 05:39:33 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
it('draft post', function (done) {
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
|
// We're going to try deleting post 4 which also has tag 4
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const firstItemData = {id: testUtils.DataGenerator.Content.posts[3].id, status: 'draft'};
|
2013-11-06 01:00:29 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
// Test that we have the post we expect, with exactly one tag
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne(firstItemData, {withRelated: ['tags']}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let post;
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
post = results.toJSON();
|
|
|
|
|
post.id.should.equal(firstItemData.id);
|
|
|
|
|
post.tags.should.have.length(1);
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
|
post.tags[0].id.should.equal(testUtils.DataGenerator.Content.tags[3].id);
|
2014-08-01 09:53:09 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
// Destroy the post
|
2016-03-02 07:42:01 +03:00
|
|
|
|
return results.destroy(firstItemData);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (response) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const deleted = response.toJSON();
|
2014-08-01 09:53:09 +04:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
should.equal(deleted.author, undefined);
|
2014-08-01 09:53:09 +04:00
|
|
|
|
|
2019-02-03 15:36:08 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(4);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['post.deleted']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['tag.detached']);
|
2019-02-03 15:36:08 +03:00
|
|
|
|
should.exist(eventsTriggered['post.tag.detached']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.detached']);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
|
2015-10-12 10:26:26 +03:00
|
|
|
|
// Double check we can't find the post again
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.findOne(firstItemData);
|
2015-10-12 10:26:26 +03:00
|
|
|
|
}).then(function (newResults) {
|
|
|
|
|
should.equal(newResults, null);
|
|
|
|
|
|
|
|
|
|
// Double check we can't find any related tags
|
|
|
|
|
return ghostBookshelf.knex.select().table('posts_tags').where('post_id', firstItemData.id);
|
|
|
|
|
}).then(function (postsTags) {
|
2016-02-08 00:27:01 +03:00
|
|
|
|
postsTags.should.be.empty();
|
2015-10-12 10:26:26 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('published page', function (done) {
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
|
// We're going to try deleting page 6 which has tag 1
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const firstItemData = {id: testUtils.DataGenerator.Content.posts[5].id};
|
2015-10-12 10:26:26 +03:00
|
|
|
|
|
|
|
|
|
// Test that we have the post we expect, with exactly one tag
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne(firstItemData, {withRelated: ['tags']}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let page;
|
2015-10-12 10:26:26 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
page = results.toJSON();
|
|
|
|
|
page.id.should.equal(firstItemData.id);
|
|
|
|
|
page.status.should.equal('published');
|
2019-09-16 13:51:54 +03:00
|
|
|
|
page.type.should.equal('page');
|
2015-10-12 10:26:26 +03:00
|
|
|
|
|
|
|
|
|
// Destroy the page
|
2016-03-02 07:42:01 +03:00
|
|
|
|
return results.destroy(firstItemData);
|
2015-10-12 10:26:26 +03:00
|
|
|
|
}).then(function (response) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const deleted = response.toJSON();
|
2015-10-12 10:26:26 +03:00
|
|
|
|
|
|
|
|
|
should.equal(deleted.author, undefined);
|
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(3);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['page.unpublished']);
|
|
|
|
|
should.exist(eventsTriggered['page.deleted']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.detached']);
|
2015-10-12 10:26:26 +03:00
|
|
|
|
|
|
|
|
|
// Double check we can't find the post again
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.findOne(firstItemData);
|
2015-10-12 10:26:26 +03:00
|
|
|
|
}).then(function (newResults) {
|
|
|
|
|
should.equal(newResults, null);
|
|
|
|
|
|
|
|
|
|
// Double check we can't find any related tags
|
|
|
|
|
return ghostBookshelf.knex.select().table('posts_tags').where('post_id', firstItemData.id);
|
|
|
|
|
}).then(function (postsTags) {
|
2016-02-08 00:27:01 +03:00
|
|
|
|
postsTags.should.be.empty();
|
2015-10-12 10:26:26 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('draft page', function (done) {
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
|
// We're going to try deleting post 7 which has tag 4
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const firstItemData = {id: testUtils.DataGenerator.Content.posts[6].id, status: 'draft'};
|
2015-10-12 10:26:26 +03:00
|
|
|
|
|
|
|
|
|
// Test that we have the post we expect, with exactly one tag
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findOne(firstItemData, {withRelated: ['tags']}).then(function (results) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let page;
|
2015-10-12 10:26:26 +03:00
|
|
|
|
should.exist(results);
|
|
|
|
|
page = results.toJSON();
|
|
|
|
|
page.id.should.equal(firstItemData.id);
|
|
|
|
|
|
|
|
|
|
// Destroy the page
|
2016-03-02 07:42:01 +03:00
|
|
|
|
return results.destroy(firstItemData);
|
2015-10-12 10:26:26 +03:00
|
|
|
|
}).then(function (response) {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const deleted = response.toJSON();
|
2015-10-12 10:26:26 +03:00
|
|
|
|
|
|
|
|
|
should.equal(deleted.author, undefined);
|
|
|
|
|
|
2019-01-08 12:48:53 +03:00
|
|
|
|
Object.keys(eventsTriggered).length.should.eql(2);
|
2017-01-25 16:47:49 +03:00
|
|
|
|
should.exist(eventsTriggered['page.deleted']);
|
2019-01-08 12:48:53 +03:00
|
|
|
|
should.exist(eventsTriggered['user.detached']);
|
2015-10-12 10:26:26 +03:00
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
|
// Double check we can't find the post again
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.findOne(firstItemData);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
}).then(function (newResults) {
|
|
|
|
|
should.equal(newResults, null);
|
|
|
|
|
|
|
|
|
|
// Double check we can't find any related tags
|
|
|
|
|
return ghostBookshelf.knex.select().table('posts_tags').where('post_id', firstItemData.id);
|
|
|
|
|
}).then(function (postsTags) {
|
2016-02-08 00:27:01 +03:00
|
|
|
|
postsTags.should.be.empty();
|
2014-08-01 09:53:09 +04:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
2015-03-24 23:23:23 +03:00
|
|
|
|
});
|
2014-08-01 09:53:09 +04:00
|
|
|
|
});
|
2017-04-19 16:53:23 +03:00
|
|
|
|
|
|
|
|
|
describe('Collision Protection', function () {
|
2018-04-25 18:13:35 +03:00
|
|
|
|
before(testUtils.fixtures.insertPostsAndTags);
|
2017-04-19 16:53:23 +03:00
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
after(function () {
|
|
|
|
|
return testUtils.truncate('posts_tags')
|
2017-04-19 16:53:23 +03:00
|
|
|
|
.then(function () {
|
2018-04-25 18:13:35 +03:00
|
|
|
|
return testUtils.truncate('tags');
|
2017-04-19 16:53:23 +03:00
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
2018-04-25 18:13:35 +03:00
|
|
|
|
return testUtils.truncate('posts');
|
2020-09-17 11:46:45 +03:00
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts_meta');
|
2018-04-25 18:13:35 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('update post title, but updated_at is out of sync', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postToUpdate = {id: testUtils.DataGenerator.Content.posts[1].id};
|
2018-04-25 18:13:35 +03:00
|
|
|
|
|
|
|
|
|
return models.Post.edit({
|
|
|
|
|
title: 'New Post Title',
|
|
|
|
|
updated_at: moment().subtract(1, 'day').format()
|
|
|
|
|
}, _.extend({}, context, {id: postToUpdate.id}))
|
2017-04-19 16:53:23 +03:00
|
|
|
|
.then(function () {
|
2018-04-25 18:13:35 +03:00
|
|
|
|
throw new Error('expected no success');
|
2017-04-19 16:53:23 +03:00
|
|
|
|
})
|
|
|
|
|
.catch(function (err) {
|
|
|
|
|
err.code.should.eql('UPDATE_COLLISION');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
it('update post tags and updated_at is out of sync', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postToUpdate = {id: testUtils.DataGenerator.Content.posts[1].id};
|
2017-04-19 16:53:23 +03:00
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
return models.Post.edit({
|
|
|
|
|
tags: [{name: 'new-tag-1'}],
|
|
|
|
|
updated_at: moment().subtract(1, 'day').format()
|
|
|
|
|
}, _.extend({}, context, {id: postToUpdate.id}))
|
2017-04-19 16:53:23 +03:00
|
|
|
|
.then(function () {
|
2018-04-25 18:13:35 +03:00
|
|
|
|
throw new Error('expected no success');
|
2017-04-19 16:53:23 +03:00
|
|
|
|
})
|
|
|
|
|
.catch(function (err) {
|
|
|
|
|
err.code.should.eql('UPDATE_COLLISION');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
it('update post authors and updated_at is out of sync', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postToUpdate = {id: testUtils.DataGenerator.Content.posts[1].id};
|
2018-03-27 17:16:15 +03:00
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
return models.Post.edit({
|
|
|
|
|
authors: [testUtils.DataGenerator.Content.users[3]],
|
|
|
|
|
updated_at: moment().subtract(1, 'day').format()
|
|
|
|
|
}, _.extend({}, context, {id: postToUpdate.id}))
|
2018-03-27 17:16:15 +03:00
|
|
|
|
.then(function () {
|
2018-04-25 18:13:35 +03:00
|
|
|
|
throw new Error('expected no success');
|
2018-03-27 17:16:15 +03:00
|
|
|
|
})
|
|
|
|
|
.catch(function (err) {
|
|
|
|
|
err.code.should.eql('UPDATE_COLLISION');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
it('update post tags and updated_at is NOT out of sync', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postToUpdate = {id: testUtils.DataGenerator.Content.posts[1].id};
|
2017-04-19 16:53:23 +03:00
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
return models.Post.edit({
|
|
|
|
|
tags: [{name: 'new-tag-1'}]
|
|
|
|
|
}, _.extend({}, context, {id: postToUpdate.id}));
|
2017-04-19 16:53:23 +03:00
|
|
|
|
});
|
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
it('update post with no changes, but updated_at is out of sync', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postToUpdate = {id: testUtils.DataGenerator.Content.posts[1].id};
|
2017-04-19 16:53:23 +03:00
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
return models.Post.edit({
|
|
|
|
|
updated_at: moment().subtract(1, 'day').format()
|
|
|
|
|
}, _.extend({}, context, {id: postToUpdate.id}));
|
2017-04-19 16:53:23 +03:00
|
|
|
|
});
|
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
it('update post with old post title, but updated_at is out of sync', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const postToUpdate = {
|
2017-04-19 16:53:23 +03:00
|
|
|
|
id: testUtils.DataGenerator.Content.posts[1].id,
|
|
|
|
|
title: testUtils.DataGenerator.forModel.posts[1].title
|
|
|
|
|
};
|
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
return models.Post.edit({
|
|
|
|
|
title: postToUpdate.title,
|
|
|
|
|
updated_at: moment().subtract(1, 'day').format()
|
|
|
|
|
}, _.extend({}, context, {id: postToUpdate.id}));
|
2017-04-19 16:53:23 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
2013-10-08 05:39:33 +04:00
|
|
|
|
});
|
|
|
|
|
|
2018-10-09 16:31:09 +03:00
|
|
|
|
describe('mobiledoc versioning', function () {
|
|
|
|
|
it('can create revisions', function () {
|
|
|
|
|
const newPost = {
|
|
|
|
|
mobiledoc: markdownToMobiledoc('a')
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return models.Post.add(newPost, context)
|
|
|
|
|
.then((createdPost) => {
|
|
|
|
|
return models.Post.findOne({id: createdPost.id, status: 'all'});
|
|
|
|
|
})
|
|
|
|
|
.then((createdPost) => {
|
|
|
|
|
should.exist(createdPost);
|
|
|
|
|
|
|
|
|
|
return createdPost.save({mobiledoc: markdownToMobiledoc('b')}, context);
|
|
|
|
|
})
|
|
|
|
|
.then((updatedPost) => {
|
|
|
|
|
updatedPost.get('mobiledoc').should.equal(markdownToMobiledoc('b'));
|
|
|
|
|
|
|
|
|
|
return models.MobiledocRevision
|
|
|
|
|
.findAll({
|
2019-07-05 14:40:43 +03:00
|
|
|
|
filter: `post_id:${updatedPost.id}`
|
2018-10-09 16:31:09 +03:00
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.then((mobiledocRevisions) => {
|
|
|
|
|
should.equal(mobiledocRevisions.length, 2);
|
|
|
|
|
|
|
|
|
|
mobiledocRevisions.toJSON()[0].mobiledoc.should.equal(markdownToMobiledoc('b'));
|
|
|
|
|
mobiledocRevisions.toJSON()[1].mobiledoc.should.equal(markdownToMobiledoc('a'));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('keeps only 10 last revisions in FIFO style', function () {
|
|
|
|
|
let revisionedPost;
|
|
|
|
|
const newPost = {
|
|
|
|
|
mobiledoc: markdownToMobiledoc('revision: 0')
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return models.Post.add(newPost, context)
|
|
|
|
|
.then((createdPost) => {
|
|
|
|
|
return models.Post.findOne({id: createdPost.id, status: 'all'});
|
|
|
|
|
})
|
|
|
|
|
.then((createdPost) => {
|
|
|
|
|
should.exist(createdPost);
|
|
|
|
|
revisionedPost = createdPost;
|
|
|
|
|
|
|
|
|
|
return sequence(_.times(11, (i) => {
|
|
|
|
|
return () => {
|
|
|
|
|
return models.Post.edit({
|
|
|
|
|
mobiledoc: markdownToMobiledoc('revision: ' + (i + 1))
|
|
|
|
|
}, _.extend({}, context, {id: createdPost.id}));
|
|
|
|
|
};
|
|
|
|
|
}));
|
|
|
|
|
})
|
|
|
|
|
.then(() => models.MobiledocRevision
|
|
|
|
|
.findAll({
|
2019-07-05 14:40:43 +03:00
|
|
|
|
filter: `post_id:${revisionedPost.id}`
|
2018-10-09 16:31:09 +03:00
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.then((mobiledocRevisions) => {
|
|
|
|
|
should.equal(mobiledocRevisions.length, 10);
|
|
|
|
|
|
|
|
|
|
mobiledocRevisions.toJSON()[0].mobiledoc.should.equal(markdownToMobiledoc('revision: 11'));
|
|
|
|
|
mobiledocRevisions.toJSON()[9].mobiledoc.should.equal(markdownToMobiledoc('revision: 2'));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('creates 2 revisions after first edit for previously unversioned post', function () {
|
|
|
|
|
let unversionedPost;
|
|
|
|
|
|
|
|
|
|
const newPost = {
|
|
|
|
|
title: 'post title',
|
|
|
|
|
mobiledoc: markdownToMobiledoc('a')
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// passing 'migrating' flag to simulate unversioned post
|
|
|
|
|
const options = Object.assign(_.clone(context), {migrating: true});
|
|
|
|
|
|
|
|
|
|
return models.Post.add(newPost, options)
|
|
|
|
|
.then((createdPost) => {
|
|
|
|
|
should.exist(createdPost);
|
|
|
|
|
unversionedPost = createdPost;
|
|
|
|
|
createdPost.get('mobiledoc').should.equal(markdownToMobiledoc('a'));
|
|
|
|
|
|
|
|
|
|
return models.MobiledocRevision
|
|
|
|
|
.findAll({
|
2019-07-05 14:40:43 +03:00
|
|
|
|
filter: `post_id:${createdPost.id}`
|
2018-10-09 16:31:09 +03:00
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.then((mobiledocRevisions) => {
|
|
|
|
|
should.equal(mobiledocRevisions.length, 0);
|
|
|
|
|
|
|
|
|
|
return models.Post.edit({
|
|
|
|
|
mobiledoc: markdownToMobiledoc('b')
|
|
|
|
|
}, _.extend({}, context, {id: unversionedPost.id}));
|
|
|
|
|
})
|
|
|
|
|
.then((editedPost) => {
|
|
|
|
|
should.exist(editedPost);
|
|
|
|
|
editedPost.get('mobiledoc').should.equal(markdownToMobiledoc('b'));
|
|
|
|
|
|
|
|
|
|
return models.MobiledocRevision
|
|
|
|
|
.findAll({
|
2019-07-05 14:40:43 +03:00
|
|
|
|
filter: `post_id:${editedPost.id}`
|
2018-10-09 16:31:09 +03:00
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.then((mobiledocRevisions) => {
|
|
|
|
|
should.equal(mobiledocRevisions.length, 2);
|
|
|
|
|
|
|
|
|
|
mobiledocRevisions.toJSON()[0].mobiledoc.should.equal(markdownToMobiledoc('b'));
|
|
|
|
|
mobiledocRevisions.toJSON()[1].mobiledoc.should.equal(markdownToMobiledoc('a'));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2014-07-23 12:32:27 +04:00
|
|
|
|
describe('Multiauthor Posts', function () {
|
2020-02-24 23:51:09 +03:00
|
|
|
|
before(testUtils.teardownDb);
|
2018-04-25 18:13:35 +03:00
|
|
|
|
|
|
|
|
|
after(function () {
|
2020-02-24 23:51:09 +03:00
|
|
|
|
return testUtils.teardownDb()
|
2018-04-25 18:13:35 +03:00
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.setup('users:roles')();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
before(testUtils.setup('posts:mu'));
|
2014-07-23 12:32:27 +04:00
|
|
|
|
|
|
|
|
|
it('can destroy multiple posts by author', function (done) {
|
|
|
|
|
// We're going to delete all posts by user 1
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const authorData = {id: testUtils.DataGenerator.Content.users[0].id};
|
2014-07-23 12:32:27 +04:00
|
|
|
|
|
2018-04-06 16:31:00 +03:00
|
|
|
|
models.Post.findAll({context: {internal: true}}).then(function (found) {
|
2019-10-14 17:11:10 +03:00
|
|
|
|
// There are 10 posts created by posts:mu fixture
|
|
|
|
|
found.length.should.equal(10);
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.destroyByAuthor(authorData);
|
2014-07-23 12:32:27 +04:00
|
|
|
|
}).then(function (results) {
|
2019-10-14 17:11:10 +03:00
|
|
|
|
// User 1 has 2 posts in the database (each user has proportionate amount)
|
|
|
|
|
// 2 = 10 / 5 (posts / users)
|
|
|
|
|
results.length.should.equal(2);
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.findAll({context: {internal: true}});
|
2014-07-23 12:32:27 +04:00
|
|
|
|
}).then(function (found) {
|
2019-10-14 17:11:10 +03:00
|
|
|
|
// Only 8 should remain
|
|
|
|
|
// 8 = 10 - 2
|
|
|
|
|
found.length.should.equal(8);
|
2014-07-23 12:32:27 +04:00
|
|
|
|
done();
|
|
|
|
|
}).catch(done);
|
|
|
|
|
});
|
2013-10-08 05:39:33 +04:00
|
|
|
|
});
|
2013-10-10 19:43:25 +04:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
describe('Post tag handling edge cases', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
let postJSON;
|
|
|
|
|
let tagJSON;
|
|
|
|
|
let editOptions;
|
|
|
|
|
const createTag = testUtils.DataGenerator.forKnex.createTag;
|
2017-11-21 16:28:05 +03:00
|
|
|
|
|
2018-04-25 18:13:35 +03:00
|
|
|
|
beforeEach(function () {
|
|
|
|
|
return testUtils.truncate('posts_tags')
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('tags');
|
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts');
|
2020-09-17 11:46:45 +03:00
|
|
|
|
})
|
|
|
|
|
.then(function () {
|
|
|
|
|
return testUtils.truncate('posts_meta');
|
2018-04-25 18:13:35 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
2018-03-27 17:16:15 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
beforeEach(function () {
|
|
|
|
|
tagJSON = [];
|
|
|
|
|
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const post = _.cloneDeep(testUtils.DataGenerator.forModel.posts[0]);
|
|
|
|
|
|
|
|
|
|
const postTags = [
|
|
|
|
|
createTag({name: 'tag1', slug: 'tag1'}),
|
|
|
|
|
createTag({name: 'tag2', slug: 'tag2'}),
|
|
|
|
|
createTag({name: 'tag3', slug: 'tag3'})
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const extraTags = [
|
|
|
|
|
createTag({name: 'existing tag a', slug: 'existing-tag-a'}),
|
|
|
|
|
createTag({name: 'existing-tag-b', slug: 'existing-tag-b'}),
|
|
|
|
|
createTag({name: 'existing_tag_c', slug: 'existing_tag_c'})
|
|
|
|
|
];
|
2017-11-21 16:28:05 +03:00
|
|
|
|
|
|
|
|
|
post.tags = postTags;
|
|
|
|
|
post.status = 'published';
|
|
|
|
|
|
|
|
|
|
return Promise.props({
|
2018-04-06 16:31:00 +03:00
|
|
|
|
post: models.Post.add(post, _.extend({}, context, {withRelated: ['tags']})),
|
|
|
|
|
tag1: models.Tag.add(extraTags[0], context),
|
|
|
|
|
tag2: models.Tag.add(extraTags[1], context),
|
|
|
|
|
tag3: models.Tag.add(extraTags[2], context)
|
2017-11-21 16:28:05 +03:00
|
|
|
|
}).then(function (result) {
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
|
postJSON = result.post.toJSON({withRelated: ['tags']});
|
2017-11-21 16:28:05 +03:00
|
|
|
|
tagJSON.push(result.tag1.toJSON());
|
|
|
|
|
tagJSON.push(result.tag2.toJSON());
|
|
|
|
|
tagJSON.push(result.tag3.toJSON());
|
|
|
|
|
editOptions = _.extend({}, context, {id: postJSON.id, withRelated: ['tags']});
|
|
|
|
|
|
|
|
|
|
// reset the eventSpy here
|
2019-01-21 19:53:44 +03:00
|
|
|
|
sinon.restore();
|
2016-06-02 20:11:54 +03:00
|
|
|
|
});
|
2017-11-21 16:28:05 +03:00
|
|
|
|
});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
it('should create the test data correctly', function (done) {
|
|
|
|
|
// creates a test tag
|
|
|
|
|
should.exist(tagJSON);
|
|
|
|
|
tagJSON.should.be.an.Array().with.lengthOf(3);
|
2017-11-28 21:41:16 +03:00
|
|
|
|
|
|
|
|
|
tagJSON[0].name.should.eql('existing tag a');
|
|
|
|
|
tagJSON[1].name.should.eql('existing-tag-b');
|
|
|
|
|
tagJSON[2].name.should.eql('existing_tag_c');
|
2017-11-21 16:28:05 +03:00
|
|
|
|
|
|
|
|
|
// creates a test post with an array of tags in the correct order
|
|
|
|
|
should.exist(postJSON);
|
|
|
|
|
postJSON.title.should.eql('HTML Ipsum');
|
|
|
|
|
should.exist(postJSON.tags);
|
|
|
|
|
postJSON.tags.should.be.an.Array().and.have.lengthOf(3);
|
2017-11-28 21:41:16 +03:00
|
|
|
|
|
|
|
|
|
postJSON.tags[0].name.should.eql('tag1');
|
|
|
|
|
postJSON.tags[1].name.should.eql('tag2');
|
|
|
|
|
postJSON.tags[2].name.should.eql('tag3');
|
2017-11-21 16:28:05 +03:00
|
|
|
|
|
|
|
|
|
done();
|
|
|
|
|
});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
it('can edit slug of existing tag', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const newJSON = _.cloneDeep(postJSON);
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
// Add an existing tag to the beginning of the array
|
|
|
|
|
newJSON.tags = [{id: postJSON.tags[0].id, slug: 'eins'}];
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
// Edit the post
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit(newJSON, editOptions).then(function (updatedPost) {
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
|
updatedPost = updatedPost.toJSON({withRelated: ['tags']});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
updatedPost.tags.should.have.lengthOf(1);
|
2017-11-28 21:41:16 +03:00
|
|
|
|
updatedPost.tags[0].name.should.eql(postJSON.tags[0].name);
|
|
|
|
|
updatedPost.tags[0].slug.should.eql('eins');
|
|
|
|
|
updatedPost.tags[0].id.should.eql(postJSON.tags[0].id);
|
2016-06-02 20:11:54 +03:00
|
|
|
|
});
|
2017-11-21 16:28:05 +03:00
|
|
|
|
});
|
2018-02-17 17:31:12 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
it('can\'t edit dates and authors of existing tag', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const newJSON = _.cloneDeep(postJSON);
|
|
|
|
|
let updatedAtFormat;
|
|
|
|
|
let createdAtFormat;
|
2017-11-21 16:28:05 +03:00
|
|
|
|
|
|
|
|
|
// Add an existing tag to the beginning of the array
|
2018-03-26 16:12:02 +03:00
|
|
|
|
newJSON.tags = [_.cloneDeep(postJSON.tags[0])];
|
|
|
|
|
newJSON.tags[0].created_at = moment().add(2, 'days').format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
|
newJSON.tags[0].updated_at = moment().add(2, 'days').format('YYYY-MM-DD HH:mm:ss');
|
2017-11-21 16:28:05 +03:00
|
|
|
|
|
2018-04-05 16:59:52 +03:00
|
|
|
|
// NOTE: this is currently only removed in the API layer
|
|
|
|
|
newJSON.tags[0].parent_id = newJSON.tags[0].parent;
|
|
|
|
|
delete newJSON.tags[0].parent;
|
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
// Edit the post
|
|
|
|
|
return Promise.delay(1000)
|
|
|
|
|
.then(function () {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit(newJSON, editOptions);
|
2017-11-21 16:28:05 +03:00
|
|
|
|
})
|
|
|
|
|
.then(function (updatedPost) {
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
|
updatedPost = updatedPost.toJSON({withRelated: ['tags']});
|
2017-11-21 16:28:05 +03:00
|
|
|
|
|
|
|
|
|
updatedPost.tags.should.have.lengthOf(1);
|
2017-11-28 21:41:16 +03:00
|
|
|
|
updatedPost.tags[0].should.have.properties({
|
2017-11-21 16:28:05 +03:00
|
|
|
|
name: postJSON.tags[0].name,
|
2018-03-26 16:12:02 +03:00
|
|
|
|
slug: postJSON.tags[0].slug,
|
2017-11-21 16:28:05 +03:00
|
|
|
|
id: postJSON.tags[0].id,
|
2018-03-26 16:12:02 +03:00
|
|
|
|
created_by: postJSON.tags[0].created_by,
|
|
|
|
|
updated_by: postJSON.tags[0].updated_by
|
2016-08-11 10:51:19 +03:00
|
|
|
|
});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
updatedAtFormat = moment(updatedPost.tags[0].updated_at).format('YYYY-MM-DD HH:mm:ss');
|
2018-03-26 16:12:02 +03:00
|
|
|
|
updatedAtFormat.should.eql(moment(postJSON.tags[0].updated_at).format('YYYY-MM-DD HH:mm:ss'));
|
2017-11-21 16:28:05 +03:00
|
|
|
|
updatedAtFormat.should.not.eql(moment(newJSON.tags[0].updated_at).format('YYYY-MM-DD HH:mm:ss'));
|
2018-03-26 16:12:02 +03:00
|
|
|
|
|
|
|
|
|
createdAtFormat = moment(updatedPost.tags[0].created_at).format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
|
createdAtFormat.should.eql(moment(postJSON.tags[0].created_at).format('YYYY-MM-DD HH:mm:ss'));
|
|
|
|
|
createdAtFormat.should.not.eql(moment(newJSON.tags[0].created_at).format('YYYY-MM-DD HH:mm:ss'));
|
2016-06-02 20:11:54 +03:00
|
|
|
|
});
|
2017-11-21 16:28:05 +03:00
|
|
|
|
});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
it('can reorder existing, added and deleted tags', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const newJSON = _.cloneDeep(postJSON);
|
|
|
|
|
const lastTag = [postJSON.tags[2]];
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
// remove tag in the middle (tag1, tag2, tag3 -> tag1, tag3)
|
|
|
|
|
newJSON.tags.splice(1, 1);
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
// add a new one as first tag and reorder existing (tag4, tag3, tag1)
|
|
|
|
|
newJSON.tags = [{name: 'tag4'}].concat([newJSON.tags[1]]).concat([newJSON.tags[0]]);
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
// Edit the post
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit(newJSON, editOptions).then(function (updatedPost) {
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
|
updatedPost = updatedPost.toJSON({withRelated: ['tags']});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
updatedPost.tags.should.have.lengthOf(3);
|
2017-11-28 21:41:16 +03:00
|
|
|
|
updatedPost.tags[0].should.have.properties({
|
2017-11-21 16:28:05 +03:00
|
|
|
|
name: 'tag4'
|
2016-06-02 20:11:54 +03:00
|
|
|
|
});
|
2017-11-28 21:41:16 +03:00
|
|
|
|
|
|
|
|
|
updatedPost.tags[1].should.have.properties({
|
2017-11-21 16:28:05 +03:00
|
|
|
|
name: 'tag3',
|
|
|
|
|
id: postJSON.tags[2].id
|
2016-06-02 20:11:54 +03:00
|
|
|
|
});
|
2017-11-28 21:41:16 +03:00
|
|
|
|
|
|
|
|
|
updatedPost.tags[2].should.have.properties({
|
2017-11-21 16:28:05 +03:00
|
|
|
|
name: 'tag1',
|
|
|
|
|
id: postJSON.tags[0].id
|
2016-06-02 20:11:54 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
it('can add multiple tags with conflicting slugs', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const newJSON = _.cloneDeep(postJSON);
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
// Add conflicting tags to the end of the array
|
|
|
|
|
newJSON.tags = [];
|
|
|
|
|
newJSON.tags.push({name: 'C'});
|
|
|
|
|
newJSON.tags.push({name: 'C++'});
|
|
|
|
|
newJSON.tags.push({name: 'C#'});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
// Edit the post
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit(newJSON, editOptions).then(function (updatedPost) {
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
|
updatedPost = updatedPost.toJSON({withRelated: ['tags']});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
updatedPost.tags.should.have.lengthOf(3);
|
2017-11-28 21:41:16 +03:00
|
|
|
|
|
|
|
|
|
updatedPost.tags[0].should.have.properties({name: 'C', slug: 'c'});
|
|
|
|
|
updatedPost.tags[1].should.have.properties({name: 'C++', slug: 'c-2'});
|
|
|
|
|
updatedPost.tags[2].should.have.properties({name: 'C#', slug: 'c-3'});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
});
|
2017-11-21 16:28:05 +03:00
|
|
|
|
});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
it('can handle lowercase/uppercase tags', function () {
|
2020-04-29 18:44:27 +03:00
|
|
|
|
const newJSON = _.cloneDeep(postJSON);
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
// Add conflicting tags to the end of the array
|
|
|
|
|
newJSON.tags = [];
|
|
|
|
|
newJSON.tags.push({name: 'test'});
|
|
|
|
|
newJSON.tags.push({name: 'tEst'});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
// Edit the post
|
2018-04-06 16:31:00 +03:00
|
|
|
|
return models.Post.edit(newJSON, editOptions).then(function (updatedPost) {
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
|
updatedPost = updatedPost.toJSON({withRelated: ['tags']});
|
2016-06-02 20:11:54 +03:00
|
|
|
|
|
2017-11-21 16:28:05 +03:00
|
|
|
|
updatedPost.tags.should.have.lengthOf(1);
|
2016-06-02 20:11:54 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2014-01-07 00:17:20 +04:00
|
|
|
|
// disabling sanitization until we can implement a better version
|
|
|
|
|
// it('should sanitize the title', function (done) {
|
2018-04-06 16:31:00 +03:00
|
|
|
|
// new models.Post().fetch().then(function (model) {
|
2014-01-07 00:17:20 +04:00
|
|
|
|
// return model.set({'title': "</title></head><body><script>alert('blogtitle');</script>"}).save();
|
|
|
|
|
// }).then(function (saved) {
|
|
|
|
|
// saved.get('title').should.eql("</title></head><body>[removed]alert('blogtitle');[removed]");
|
|
|
|
|
// done();
|
2014-08-17 10:17:23 +04:00
|
|
|
|
// }).catch(done);
|
2014-01-07 00:17:20 +04:00
|
|
|
|
// });
|
2013-10-08 05:39:33 +04:00
|
|
|
|
});
|