const errors = require('@tryghost/errors'); const should = require('should'); const sinon = require('sinon'); const testUtils = require('../../utils'); const moment = require('moment'); const _ = require('lodash'); const Promise = require('bluebird'); const {sequence} = require('@tryghost/promise'); const urlService = require('../../../core/server/services/url'); const ghostBookshelf = require('../../../core/server/models/base'); const models = require('../../../core/server/models'); const db = require('../../../core/server/data/db'); const settingsCache = require('../../../core/shared/settings-cache'); const events = require('../../../core/server/lib/common/events'); const configUtils = require('../../utils/configUtils'); const context = testUtils.context.owner; const markdownToMobiledoc = testUtils.DataGenerator.markdownToMobiledoc; /** * 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 */ describe('Post Model', function () { let eventsTriggered = {}; before(testUtils.teardownDb); before(testUtils.stopGhost); after(testUtils.teardownDb); before(testUtils.setup('users:roles')); afterEach(function () { sinon.restore(); }); beforeEach(function () { sinon.stub(urlService, 'getUrlByResourceId').withArgs(testUtils.DataGenerator.Content.posts[0].id).returns('/html-ipsum/'); }); describe('Single author posts', function () { afterEach(function () { configUtils.restore(); }); 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'); }) .then(function () { return testUtils.truncate('posts_meta'); }); }); 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); }); }); describe('findPage', function () { describe('with more posts/tags', function () { beforeEach(function () { return testUtils.truncate('posts_tags') .then(function () { return testUtils.truncate('tags'); }) .then(function () { return testUtils.truncate('posts_meta'); }) .then(function () { return testUtils.truncate('posts'); }); }); beforeEach(function () { return testUtils.fixtures.insertPostsAndTags() .then(function () { return testUtils.fixtures.insertExtraPosts(); }) .then(function () { return testUtils.fixtures.insertExtraPostsTags(); }); }); it('can findPage, with various options', function (done) { models.Post.findPage({page: 2}) .then(function (paginationResult) { 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); }); it('can findPage for tag, with various options', function (done) { // Test tag filter models.Post.findPage({page: 1, filter: 'tags:bacon'}) .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: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); }); }); }); }); describe('edit', function () { beforeEach(testUtils.fixtures.insertPostsAndTags); afterEach(function () { return testUtils.truncate('posts_tags') .then(function () { return testUtils.truncate('tags'); }) .then(function () { return testUtils.truncate('posts'); }) .then(function () { return testUtils.truncate('posts_meta'); }); }); beforeEach(function () { eventsTriggered = {}; sinon.stub(events, 'emit').callsFake(function (eventName, eventObj) { if (!eventsTriggered[eventName]) { eventsTriggered[eventName] = []; } eventsTriggered[eventName].push(eventObj); }); }); it('[failure] multiple edits in one transaction', function () { const options = _.cloneDeep(context); const data = { status: 'published' }; 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 () { const options = _.cloneDeep(context); const data = { status: 'published' }; 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 Object.keys(eventsTriggered).length.should.eql(4); }); }); it('can change title', function (done) { const postId = testUtils.DataGenerator.Content.posts[0].id; models.Post.findOne({id: postId}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.id.should.equal(postId); post.title.should.not.equal('new title'); return models.Post.edit({title: 'new title'}, _.extend({}, context, {id: postId})); }).then(function (edited) { should.exist(edited); edited.attributes.title.should.equal('new title'); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.published.edited']); should.exist(eventsTriggered['post.edited']); done(); }).catch(done); }); it('[failure] custom excerpt soft limit reached', function (done) { const postId = testUtils.DataGenerator.Content.posts[0].id; models.Post.findOne({id: postId}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.id.should.equal(postId); return models.Post.edit({ custom_excerpt: new Array(302).join('a') }, _.extend({}, context, {id: postId})); }).then(function () { done(new Error('expected validation error')); }).catch(function (err) { err[0].name.should.eql('ValidationError'); done(); }); }); it('can publish draft post', function (done) { const postId = testUtils.DataGenerator.Content.posts[3].id; models.Post.findOne({id: postId, status: 'draft'}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.id.should.equal(postId); post.status.should.equal('draft'); return models.Post.edit({status: 'published'}, _.extend({}, context, {id: postId})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('published'); Object.keys(eventsTriggered).length.should.eql(4); should.exist(eventsTriggered['post.published']); should.exist(eventsTriggered['post.edited']); should.exist(eventsTriggered['tag.attached']); should.exist(eventsTriggered['user.attached']); done(); }).catch(done); }); it('can unpublish published post', function (done) { const postId = testUtils.DataGenerator.Content.posts[0].id; models.Post.findOne({id: postId}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.id.should.equal(postId); post.status.should.equal('published'); return models.Post.edit({status: 'draft'}, _.extend({}, context, {id: postId})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('draft'); Object.keys(eventsTriggered).length.should.eql(4); should.exist(eventsTriggered['post.unpublished']); should.exist(eventsTriggered['post.edited']); done(); }).catch(done); }); it('draft -> scheduled without published_at update', function (done) { let post; models.Post.findOne({status: 'draft'}).then(function (results) { should.exist(results); post = results.toJSON(); post.status.should.equal('draft'); results.set('published_at', null); return results.save(); }).then(function () { return models.Post.edit({ status: 'scheduled' }, _.extend({}, context, {id: post.id})); }).then(function () { done(new Error('expected error')); }).catch(function (err) { should.exist(err); (err instanceof errors.ValidationError).should.eql(true); done(); }); }); it('draft -> scheduled: expect update of published_at', function (done) { const newPublishedAt = moment().add(1, 'day').toDate(); models.Post.findOne({status: 'draft'}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.status.should.equal('draft'); return models.Post.edit({ 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); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.scheduled']); should.exist(eventsTriggered['post.edited']); done(); }).catch(done); }); it('scheduled -> draft: expect unschedule', function (done) { models.Post.findOne({status: 'scheduled'}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.status.should.equal('scheduled'); return models.Post.edit({ status: 'draft' }, _.extend({}, context, {id: post.id})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('draft'); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.unscheduled']); should.exist(eventsTriggered['post.edited']); done(); }).catch(done); }); it('scheduled -> scheduled with updated published_at', function (done) { models.Post.findOne({status: 'scheduled'}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.status.should.equal('scheduled'); return models.Post.edit({ status: 'scheduled', published_at: moment().add(20, 'days').toDate() }, _.extend({}, context, {id: post.id})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('scheduled'); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.rescheduled']); should.exist(eventsTriggered['post.edited']); done(); }).catch(done); }); it('scheduled -> scheduled with unchanged published_at', function (done) { models.Post.findOne({status: 'scheduled'}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.status.should.equal('scheduled'); return models.Post.edit({ status: 'scheduled' }, _.extend({}, context, {id: post.id})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('scheduled'); // nothing has changed Object.keys(eventsTriggered).length.should.eql(0); done(); }).catch(done); }); it('scheduled -> scheduled with unchanged published_at (within the 2 minutes window)', function (done) { let post; models.Post.findOne({status: 'scheduled'}).then(function (results) { 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 () { return models.Post.edit({ 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); }); it('published -> scheduled and expect update of published_at', function (done) { const postId = testUtils.DataGenerator.Content.posts[0].id; models.Post.findOne({id: postId}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.id.should.equal(postId); post.status.should.equal('published'); return models.Post.edit({ status: 'scheduled', published_at: moment().add(1, 'day').toDate() }, _.extend({}, context, {id: postId})); }).then(function () { done(new Error('change status from published to scheduled is not allowed right now!')); }).catch(function (err) { should.exist(err); (err instanceof errors.ValidationError).should.eql(true); done(); }); }); it('can convert draft post to page and back', function (done) { const postId = testUtils.DataGenerator.Content.posts[3].id; models.Post.findOne({id: postId, status: 'draft'}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.id.should.equal(postId); post.status.should.equal('draft'); return models.Post.edit({type: 'page'}, _.extend({}, context, {id: postId})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('draft'); edited.attributes.type.should.equal('page'); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.deleted']); should.exist(eventsTriggered['page.added']); return models.Post.edit({type: 'post'}, _.extend({}, context, {id: postId})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('draft'); edited.attributes.type.should.equal('post'); 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']); done(); }).catch(done); }); it('can convert draft to schedule AND post to page and back', function (done) { models.Post.findOne({status: 'draft'}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.status.should.equal('draft'); return models.Post.edit({ type: 'page', 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'); edited.attributes.type.should.equal('page'); Object.keys(eventsTriggered).length.should.eql(3); should.exist(eventsTriggered['post.deleted']); should.exist(eventsTriggered['page.added']); should.exist(eventsTriggered['page.scheduled']); return models.Post.edit({type: 'post'}, _.extend({}, context, {id: edited.id})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('scheduled'); edited.attributes.type.should.equal('post'); 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']); done(); }).catch(done); }); it('can convert published post to page and back', function (done) { const postId = testUtils.DataGenerator.Content.posts[0].id; models.Post.findOne({id: postId}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.id.should.equal(postId); post.status.should.equal('published'); return models.Post.edit({type: 'page'}, _.extend({}, context, {id: postId})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('published'); edited.attributes.type.should.equal('page'); 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']); return models.Post.edit({type: 'post'}, _.extend({}, context, {id: postId})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('published'); edited.attributes.type.should.equal('post'); 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']); done(); }).catch(done); }); it('can change type and status at the same time', function (done) { const postId = testUtils.DataGenerator.Content.posts[3].id; models.Post.findOne({id: postId, status: 'draft'}).then(function (results) { let post; should.exist(results); post = results.toJSON(); post.id.should.equal(postId); post.status.should.equal('draft'); return models.Post.edit({type: 'page', status: 'published'}, _.extend({}, context, {id: postId})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('published'); edited.attributes.type.should.equal('page'); Object.keys(eventsTriggered).length.should.eql(5); should.exist(eventsTriggered['post.deleted']); should.exist(eventsTriggered['page.added']); should.exist(eventsTriggered['page.published']); should.exist(eventsTriggered['tag.attached']); should.exist(eventsTriggered['user.attached']); return models.Post.edit({type: 'post', status: 'draft'}, _.extend({}, context, {id: postId})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('draft'); edited.attributes.type.should.equal('post'); Object.keys(eventsTriggered).length.should.eql(8); should.exist(eventsTriggered['page.unpublished']); should.exist(eventsTriggered['page.deleted']); should.exist(eventsTriggered['post.added']); done(); }).catch(done); }); it('cannot override the published_by setting', function (done) { const postId = testUtils.DataGenerator.Content.posts[3].id; models.Post.findOne({id: postId, status: 'draft'}).then(function (results) { let post; 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 return models.Post.edit({ status: 'published', published_by: 4 }, _.extend({}, context, {id: postId})); }).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 return models.Post.edit({published_by: 4}, _.extend({}, context, {id: postId})); }).then(function (edited) { should.exist(edited); edited.attributes.status.should.equal('published'); edited.attributes.published_by.should.equal(context.context.user); done(); }).catch(done); }); 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); }); 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); }); }); describe('add', function () { before(testUtils.fixtures.insertPostsAndTags); after(function () { return testUtils.truncate('posts_tags') .then(function () { return testUtils.truncate('tags'); }) .then(function () { return testUtils.truncate('posts'); }) .then(function () { return testUtils.truncate('posts_meta'); }); }); beforeEach(function () { eventsTriggered = {}; sinon.stub(events, 'emit').callsFake(function (eventName, eventObj) { if (!eventsTriggered[eventName]) { eventsTriggered[eventName] = []; } eventsTriggered[eventName].push(eventObj); }); }); it('can add, defaults are all correct', function (done) { let createdPostUpdatedDate; const newPost = testUtils.DataGenerator.forModel.posts[2]; const newPostDB = testUtils.DataGenerator.Content.posts[2]; 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/); createdPost.get('slug').should.equal(newPostDB.slug + '-2'); (!!createdPost.get('featured')).should.equal(false); (!!createdPost.get('page')).should.equal(false); should.equal(createdPost.get('locale'), null); 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) { const originalSettingsCacheGetFn = settingsCache.get; 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); }); let createdPostUpdatedDate; const newPost = testUtils.DataGenerator.forModel.posts[2]; const newPostDB = testUtils.DataGenerator.Content.posts[2]; 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/); // createdPost.get('slug').should.equal(newPostDB.slug + '-3'); (!!createdPost.get('featured')).should.equal(false); (!!createdPost.get('page')).should.equal(false); should.equal(createdPost.get('locale'), null); should.equal(createdPost.get('visibility'), 'paid'); // 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, with previous published_at date', function (done) { const previousPublishedAtDate = new Date(2013, 8, 21, 12); models.Post.add({ status: 'published', published_at: previousPublishedAtDate, title: 'published_at test', mobiledoc: markdownToMobiledoc('This is some content') }, context).then(function (newPost) { should.exist(newPost); new Date(newPost.get('published_at')).getTime().should.equal(previousPublishedAtDate.getTime()); Object.keys(eventsTriggered).length.should.eql(3); should.exist(eventsTriggered['post.added']); should.exist(eventsTriggered['post.published']); should.exist(eventsTriggered['user.attached']); done(); }).catch(done); }); it('add draft post without published_at -> we expect no auto insert of published_at', function (done) { models.Post.add({ status: 'draft', title: 'draft 1', mobiledoc: markdownToMobiledoc('This is some content') }, context).then(function (newPost) { should.exist(newPost); should.not.exist(newPost.get('published_at')); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.added']); should.exist(eventsTriggered['user.attached']); done(); }).catch(done); }); it('add multiple authors', function (done) { models.Post.add({ 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); }); it('add draft post with published_at -> we expect published_at to exist', function (done) { models.Post.add({ status: 'draft', published_at: moment().toDate(), title: 'draft 1', mobiledoc: markdownToMobiledoc('This is some content') }, context).then(function (newPost) { should.exist(newPost); should.exist(newPost.get('published_at')); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.added']); should.exist(eventsTriggered['user.attached']); done(); }).catch(done); }); it('add scheduled post without published_at -> we expect an error', function (done) { models.Post.add({ status: 'scheduled', title: 'scheduled 1', mobiledoc: markdownToMobiledoc('This is some content') }, context).catch(function (err) { should.exist(err); (err instanceof errors.ValidationError).should.eql(true); Object.keys(eventsTriggered).length.should.eql(0); done(); }); }); it('add scheduled post with published_at not in future-> we expect an error', function (done) { models.Post.add({ status: 'scheduled', published_at: moment().subtract(1, 'minute'), title: 'scheduled 1', mobiledoc: markdownToMobiledoc('This is some content') }, context).catch(function (err) { should.exist(err); (err instanceof errors.ValidationError).should.eql(true); Object.keys(eventsTriggered).length.should.eql(0); done(); }); }); it('add scheduled post with published_at 1 minutes in future -> we expect an error', function (done) { models.Post.add({ status: 'scheduled', published_at: moment().add(1, 'minute'), title: 'scheduled 1', mobiledoc: markdownToMobiledoc('This is some content') }, context).catch(function (err) { (err instanceof errors.ValidationError).should.eql(true); Object.keys(eventsTriggered).length.should.eql(0); done(); }); }); it('add scheduled post with published_at 10 minutes in future -> we expect success', function (done) { models.Post.add({ status: 'scheduled', published_at: moment().add(10, 'minute'), title: 'scheduled 1', mobiledoc: markdownToMobiledoc('This is some content') }, context).then(function (post) { should.exist(post); Object.keys(eventsTriggered).length.should.eql(3); should.exist(eventsTriggered['post.added']); should.exist(eventsTriggered['post.scheduled']); should.exist(eventsTriggered['user.attached']); done(); }).catch(done); }); it('can generate a non conflicting slug', function (done) { // Create 12 posts with the same title sequence(_.times(12, function (i) { return function () { return models.Post.add({ title: 'Test Title', mobiledoc: markdownToMobiledoc('Test Content ' + (i + 1)) }, 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) { const num = i + 1; // First one has normal title if (num === 1) { post.get('slug').should.equal('test-title'); return; } post.get('slug').should.equal('test-title-' + num); JSON.parse(post.get('mobiledoc')).cards[0][1].markdown.should.equal('Test Content ' + num); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.added']); should.exist(eventsTriggered['user.attached']); eventsTriggered['post.added'].length.should.eql(12); }); done(); }).catch(done); }); it('can generate slugs without duplicate hyphens', function (done) { const newPost = { title: 'apprehensive titles have too many spaces—and m-dashes — – and also n-dashes ', mobiledoc: markdownToMobiledoc('Test Content 1') }; models.Post.add(newPost, context).then(function (createdPost) { createdPost.get('slug').should.equal('apprehensive-titles-have-too-many-spaces-and-m-dashes-and-also-n-dashes'); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.added']); should.exist(eventsTriggered['user.attached']); done(); }).catch(done); }); it('can generate a safe slug when a protected keyword is used', function (done) { const newPost = { title: 'rss', mobiledoc: markdownToMobiledoc('Test Content 1') }; models.Post.add(newPost, context).then(function (createdPost) { createdPost.get('slug').should.not.equal('rss'); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.added']); should.exist(eventsTriggered['user.attached']); done(); }); }); it('can generate slugs without non-ascii characters', function (done) { const newPost = { title: 'भुते धडकी भरवणारा आहेत', mobiledoc: markdownToMobiledoc('Test Content 1') }; models.Post.add(newPost, context).then(function (createdPost) { createdPost.get('slug').should.equal('bhute-dhddkii-bhrvnnaaraa-aahet'); done(); }).catch(done); }); it('detects duplicate slugs before saving', function (done) { const firstPost = { title: 'First post', mobiledoc: markdownToMobiledoc('First content 1') }; const secondPost = { title: 'Second post', mobiledoc: markdownToMobiledoc('Second content 1') }; // Create the first post models.Post.add(firstPost, context) .then(function (createdFirstPost) { // Store the slug for later firstPost.slug = createdFirstPost.get('slug'); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.added']); should.exist(eventsTriggered['user.attached']); // Create the second post return models.Post.add(secondPost, context); }).then(function (createdSecondPost) { // Store the slug for comparison later secondPost.slug = createdSecondPost.get('slug'); Object.keys(eventsTriggered).length.should.eql(2); should.exist(eventsTriggered['post.added']); should.exist(eventsTriggered['user.attached']); // Update with a conflicting slug from the first post return createdSecondPost.save({ slug: firstPost.slug }, context); }).then(function (updatedSecondPost) { // Should have updated from original 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); Object.keys(eventsTriggered).length.should.eql(3); should.exist(eventsTriggered['post.edited']); return models.Post.findOne({ id: updatedSecondPost.id, status: 'all' }); }).then(function (foundPost) { // Should have updated from original 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); done(); }).catch(done); }); it('it stores urls as transform-ready and reads as absolute', function (done) { const post = { title: 'Absolute->Transform-ready URL Transform Test', 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 links in custom excerpts', codeinjection_head: '', codeinjection_foot: '', feature_image: 'http://127.0.0.1:2369/content/images/feature.png', 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' } }; models.Post.add(post, context).then((createdPost) => { 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('