Renamed newsletter_id and email_recipient_filter options (#14798)

refs https://github.com/TryGhost/Team/issues/1596

- Renamed `newsletter_id` to `newsletter` option, the `newsletter` option expects a slug instead of an id
- Renamed `email_recipient_filter` to `email_segment` option
- Default `email_segment` to `all`. Ignored if no newsletter is set
- `email_segment` is ignored if no newsletter is set
- When reverting a post to a draft, both `newsletter` and `email_segment` are reset to their default values (null, all)
- Removed legacy mapping from old email_recipient_filter values 'paid' and 'free' (already a migration in place)
- Dropped legacy throwing errors when email_recipient_filter is paid or free in transformEmailRecipientFilter
- Reorganized transformEmailRecipientFilter parameters for the now required newsletter parameter
- Fixed an issue where the newsletter filter wasn't working because it wasn't in permittedoptions
- Fixed an issue where you could send to an archived newsletter
- Added an extra protection when scheduling to an active, and later archiving the newsletter
- Dropped support for `send_email_when_published` in API
- When importing posts we currently don't have a system in place to set the newsletter_id to map the `send_email_when_published` behaviour. Since this was already the case, I won't include a fix in this PR.
- Stripped `email_recipient_filter`/`email_segment` from Content API (https://ghost.slack.com/archives/C02G9E68C/p1652363211841359?thread_ts=1650623650.233229&cid=C02G9E68C)
- Updated `admin-api-schema` to 3.2.0, which includes the new email_segment property
- Contains a temporary fix for https://github.com/TryGhost/Team/issues/1626, where the `.related('newsletter').fetch` call fails when the newsletter relation is already loaded, because of the overridden `formatOnWrite` method.

Since the `email_recipient_filter` is no longer used without a newsletter, the `none` value is no longer used. A migration transforms all those values to `all`. This should be safe, because we only send an email now when newsletter_id is not null (scheduled posts should already have a newsletter_id, even if at the time of scheduling they didn't add the newsletter_id option, because at that time, we defaulted to the default newsletter).

Admin changes to make this work: https://github.com/TryGhost/Admin/pull/2380
This commit is contained in:
Simon Backx 2022-05-16 10:18:04 +02:00 committed by GitHub
parent f83ceb80d6
commit 6b3a657f88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 905 additions and 604 deletions

View File

@ -128,9 +128,8 @@ module.exports = {
'id',
'formats',
'source',
'email_recipient_filter',
'newsletter_id',
'send_email_when_published',
'email_segment',
'newsletter',
'force_rerender',
// NOTE: only for internal context
'forUpdate',
@ -146,9 +145,6 @@ module.exports = {
},
source: {
values: ['html']
},
send_email_when_published: {
values: [true, false]
}
}
},

View File

@ -102,15 +102,6 @@ const forceStatusFilter = (frame) => {
}
};
const transformLegacyEmailRecipientFilters = (frame) => {
if (frame.options.email_recipient_filter === 'free') {
frame.options.email_recipient_filter = 'status:free';
}
if (frame.options.email_recipient_filter === 'paid') {
frame.options.email_recipient_filter = 'status:-free';
}
};
module.exports = {
browse(apiConfig, frame) {
debug('browse');
@ -205,7 +196,6 @@ module.exports = {
});
}
transformLegacyEmailRecipientFilters(frame);
handlePostsMeta(frame);
defaultFormat(frame);
defaultRelations(frame);

View File

@ -4,7 +4,7 @@ module.exports = async (model, frame, options) => {
const jsonModel = await mapPost(model, frame, options);
delete jsonModel.email_subject;
delete jsonModel.email_recipient_filter;
delete jsonModel.email_segment;
delete jsonModel.email_only;
delete jsonModel.newsletter_id;

View File

@ -26,6 +26,10 @@ module.exports = async (model, frame, options = {}) => {
const jsonModel = model.toJSON(extendedOptions);
// Map email_recipient_filter to email_segment
jsonModel.email_segment = jsonModel.email_recipient_filter;
delete jsonModel.email_recipient_filter;
url.forPost(model.id, jsonModel, frame);
extraAttrs.forPost(frame, model, jsonModel);
@ -57,8 +61,6 @@ module.exports = async (model, frame, options = {}) => {
}
date.forPost(jsonModel);
gating.forPost(jsonModel, frame);
delete jsonModel.newsletter_id;
}
// Transforms post/page metadata to flat structure

View File

@ -78,6 +78,7 @@ const post = (attrs, frame) => {
delete attrs.status;
delete attrs.email_only;
delete attrs.newsletter;
delete attrs.email_segment;
// We are standardising on returning null from the Content API for any empty values
if (attrs.twitter_title === '') {
@ -106,12 +107,8 @@ const post = (attrs, frame) => {
delete attrs.page;
}
if (columns && columns.includes('email_recipient_filter') && fields && !fields.includes('email_recipient_filter')) {
delete attrs.email_recipient_filter;
}
if (fields && !fields.includes('send_email_when_published')) {
delete attrs.send_email_when_published;
if (columns && columns.includes('email_segment') && fields && !fields.includes('email_segment')) {
delete attrs.email_segment;
}
if (!attrs.tags) {

View File

@ -37,9 +37,8 @@ class PostsImporter extends BaseImporter {
if (_.has(obj, 'send_email_when_published')) {
if (obj.send_email_when_published) {
obj.email_recipient_filter = obj.visibility === 'paid' ? 'paid' : 'all';
} else {
obj.email_recipient_filter = 'none';
obj.email_recipient_filter = obj.visibility === 'paid' ? 'status:-free' : 'all';
// @TODO: we need to set the newsletter_id to the default newsletter here to have a proper fallback for older imports
}
delete obj.send_email_when_published;
}

View File

@ -0,0 +1,28 @@
const logging = require('@tryghost/logging');
const {createTransactionalMigration} = require('../../utils');
module.exports = createTransactionalMigration(
async function up(knex) {
// The 'none' value is no longer supported/used
logging.info(`Updating posts with email_recipient_filter 'none' to 'all' and newsletter_id to null`);
const affectedRows = await knex('posts')
.update({
email_recipient_filter: 'all',
newsletter_id: null
})
.where('email_recipient_filter', 'none');
logging.info(`Updated ${affectedRows} posts' email_recipient_filter to 'all' and newsletter_id to null`);
},
async function down(knex) {
// In previous versions, none meant that no email was sent.
logging.info(`Updating posts' email_recipient_filter to 'none' if they have no newsletter`);
const affectedRows = await knex('posts')
.update('email_recipient_filter', 'none')
.where('newsletter_id', null);
logging.info(`Updated ${affectedRows} posts' email_recipient_filter to 'none'`);
}
);

View File

@ -104,6 +104,28 @@ const Newsletter = ghostBookshelf.Model.extend({
return attrs;
}
}, {
/**
* Returns an array of keys permitted in a method's `options` hash, depending on the current method.
* @param {String} methodName The name of the method to check valid options for.
* @return {Array} Keys allowed in the `options` hash of the model's method.
*/
permittedOptions: function permittedOptions(methodName) {
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
// allowlists for the `options` hash argument on methods, by method name.
// these are the only options that can be passed to Bookshelf / Knex.
const validOptions = {
findOne: ['filter'],
findAll: ['filter']
};
if (validOptions[methodName]) {
options = options.concat(validOptions[methodName]);
}
return options;
},
orderDefaultRaw: function () {
return 'sort_order ASC, created_at ASC, id ASC';
},

View File

@ -16,13 +16,16 @@ const mobiledocLib = require('../lib/mobiledoc');
const relations = require('./relations');
const urlUtils = require('../../shared/url-utils');
const {Tag} = require('./tag');
const {Newsletter} = require('./newsletter');
const {BadRequestError} = require('@tryghost/errors');
const messages = {
isAlreadyPublished: 'Your post is already published, please reload your page.',
valueCannotBeBlank: 'Value in {key} cannot be blank.',
expectedPublishedAtInFuture: 'Date must be at least {cannotScheduleAPostBeforeInMinutes} minutes in the future.',
untitled: '(Untitled)',
notEnoughPermission: 'You do not have permission to perform this action'
notEnoughPermission: 'You do not have permission to perform this action',
invalidNewsletter: 'The newsletter parameter doesn\'t match any active newsletter.'
};
const MOBILEDOC_REVISIONS_COUNT = 10;
@ -79,7 +82,7 @@ Post = ghostBookshelf.Model.extend({
type: 'post',
tiers,
visibility: visibility,
email_recipient_filter: 'none'
email_recipient_filter: 'all'
};
},
@ -136,14 +139,6 @@ Post = ghostBookshelf.Model.extend({
}
});
// update legacy email_recipient_filter values to proper NQL
if (attrs.email_recipient_filter === 'free') {
attrs.email_recipient_filter = 'status:free';
}
if (attrs.email_recipient_filter === 'paid') {
attrs.email_recipient_filter = 'status:-free';
}
return attrs;
},
@ -187,14 +182,6 @@ Post = ghostBookshelf.Model.extend({
}
});
// update legacy email_recipient_filter values to proper NQL
if (attrs.email_recipient_filter === 'free') {
attrs.email_recipient_filter = 'status:free';
}
if (attrs.email_recipient_filter === 'paid') {
attrs.email_recipient_filter = 'status:-free';
}
// transform visibility NQL queries to special-case values where necessary
// ensures checks against special-case values such as `{{#has visibility="paid"}}` continue working
if (attrs.visibility && !['public', 'members', 'paid', 'tiers'].includes(attrs.visibility)) {
@ -675,20 +662,30 @@ Post = ghostBookshelf.Model.extend({
}
}
// newsletter_id is read-only and should only be set using a query param when publishing/scheduling
if (options.newsletter_id
// newsletter_id is read-only and should only be set using the newsletter param when publishing/scheduling
if (options.newsletter
&& !this.get('newsletter_id')
&& this.hasChanged('status')
&& (newStatus === 'published' || newStatus === 'scheduled')) {
this.set('newsletter_id', options.newsletter_id);
}
// Map the passed slug to the id + validate the passed newsletter
ops.push(async () => {
const newsletter = await Newsletter.findOne({slug: options.newsletter}, {transacting: options.transacting, filter: 'status:active'});
if (!newsletter) {
throw new BadRequestError({
message: messages.invalidNewsletter
});
}
this.set('newsletter_id', newsletter.id);
});
// email_recipient_filter is read-only and should only be set using a query param when publishing/scheduling
if (options.email_recipient_filter
&& (options.email_recipient_filter !== 'none')
&& this.hasChanged('status')
&& (newStatus === 'published' || newStatus === 'scheduled')) {
this.set('email_recipient_filter', options.email_recipient_filter);
// If the `email_segment` isn't passed at the same time, reset it to be 100% sure that they can only be used together
this.set('email_recipient_filter', 'all');
// email_segment is read-only and should only be set using a query param when publishing/scheduling
// we can't set it if we don't pass newsletter
if (options.email_segment) {
this.set('email_recipient_filter', options.email_segment);
}
}
// ensure draft posts have the email_recipient_filter reset unless an email has already been sent
@ -696,7 +693,7 @@ Post = ghostBookshelf.Model.extend({
ops.push(function ensureSendEmailWhenPublishedIsUnchanged() {
return self.related('email').fetch({transacting: options.transacting}).then((email) => {
if (!email) {
self.set('email_recipient_filter', 'none');
self.set('email_recipient_filter', 'all');
self.set('newsletter_id', null);
}
});
@ -1030,7 +1027,7 @@ Post = ghostBookshelf.Model.extend({
findPage: ['status'],
findAll: ['columns', 'filter'],
destroy: ['destroyAll', 'destroyBy'],
edit: ['filter', 'email_recipient_filter', 'force_rerender', 'newsletter_id']
edit: ['filter', 'email_segment', 'force_rerender', 'newsletter']
};
// The post model additionally supports having a formats option

View File

@ -8,7 +8,7 @@ class EmailPreview {
* @returns {Promise<Object>}
*/
async generateEmailContent(post, memberSegment) {
let newsletter = await post.related('newsletter').fetch();
let newsletter = post.relations.newsletter ?? await post.related('newsletter').fetch();
if (!newsletter) {
newsletter = await models.Newsletter.getDefaultNewsletter();
}

View File

@ -17,7 +17,6 @@ const models = require('../../models');
const postEmailSerializer = require('./post-email-serializer');
const labs = require('../../../shared/labs');
const {getSegmentsFromHtml} = require('./segment-parser');
const labsService = require('../../../shared/labs');
// Used to listen to email.added and email.edited model events originally, I think to offload this - ideally would just use jobs now if possible
const events = require('../../lib/common/events');
@ -28,6 +27,7 @@ const messages = {
noneFilterError: 'Cannot send email to "none" {property}',
emailSendingDisabled: `Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org`,
sendEmailRequestFailed: 'The email service was unable to send an email batch.',
archivedNewsletterError: 'Cannot send email to archived newsletters',
newsletterVisibilityError: 'Unexpected visibility value "{value}". Use one of the valid: "members", "paid".'
};
@ -53,8 +53,9 @@ const getReplyToAddress = (fromAddress, replyAddressOption) => {
* @param {Object} options
*/
const getEmailData = async (postModel, options) => {
let newsletter = await postModel.related('newsletter').fetch();
let newsletter = postModel.relations.newsletter ?? await postModel.related('newsletter').fetch();
if (!newsletter) {
// The postModel doesn't have a newsletter in test emails
newsletter = await models.Newsletter.getDefaultNewsletter();
}
const {subject, html, plaintext} = await postEmailSerializer.serialize(postModel, newsletter, options);
@ -132,28 +133,14 @@ const sendTestEmail = async (postModel, toEmails, memberSegment) => {
*
* Accepts a filter string, errors on unexpected legacy filter syntax and enforces subscribed:true
*
* @param {Object} newsletter
* @param {string} emailRecipientFilter NQL filter for members
* @param {object} options
* @param {string} errorProperty
*/
const transformEmailRecipientFilter = (emailRecipientFilter, {errorProperty = 'email_recipient_filter'} = {}, newsletter = null) => {
let filter = [];
if (!newsletter) {
filter.push(`subscribed:true`);
} else {
filter.push(`newsletters.id:${newsletter.id}`);
}
const transformEmailRecipientFilter = (newsletter, emailRecipientFilter, errorProperty) => {
const filter = [`newsletters.id:${newsletter.id}`];
switch (emailRecipientFilter) {
// `paid` and `free` were swapped out for NQL filters in 4.5.0, we shouldn't see them here now
case 'paid':
case 'free':
throw new errors.InternalServerError({
message: tpl(messages.unexpectedFilterError, {
property: errorProperty,
value: emailRecipientFilter
})
});
case 'all':
break;
case 'none':
@ -167,22 +154,20 @@ const transformEmailRecipientFilter = (emailRecipientFilter, {errorProperty = 'e
break;
}
if (newsletter) {
const visibility = newsletter.get('visibility');
switch (visibility) {
case 'members':
// No need to add a member status filter as the email is available to all members
break;
case 'paid':
filter.push(`status:-free`);
break;
default:
throw new errors.InternalServerError({
message: tpl(messages.newsletterVisibilityError, {
value: visibility
})
});
}
const visibility = newsletter.get('visibility');
switch (visibility) {
case 'members':
// No need to add a member status filter as the email is available to all members
break;
case 'paid':
filter.push(`status:-free`);
break;
default:
throw new errors.InternalServerError({
message: tpl(messages.newsletterVisibilityError, {
value: visibility
})
});
}
return filter.join('+');
@ -196,7 +181,6 @@ const transformEmailRecipientFilter = (emailRecipientFilter, {errorProperty = 'e
*
* @param {object} postModel Post Model Object
* @param {object} options
* @param {string} options.newsletter_id - the newsletter_id to send the email to
*/
const addEmail = async (postModel, options) => {
@ -213,10 +197,19 @@ const addEmail = async (postModel, options) => {
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
const filterOptions = {...knexOptions, limit: 1};
const newsletter = await postModel.related('newsletter').fetch({require: true, ..._.pick(options, ['transacting'])});
// TODO: this is a hack for https://github.com/TryGhost/Team/issues/1626
const newsletter = postModel.relations.newsletter ?? await postModel.related('newsletter').fetch({require: true, ..._.pick(options, ['transacting'])});
if (newsletter.get('status') !== 'active') {
// A post might have been scheduled to an archived newsletter.
// Don't send it (people can't unsubscribe any longer).
throw new errors.EmailError({
message: tpl(messages.archivedNewsletterError)
});
}
const emailRecipientFilter = postModel.get('email_recipient_filter');
filterOptions.filter = transformEmailRecipientFilter(emailRecipientFilter, {errorProperty: 'email_recipient_filter'}, newsletter);
filterOptions.filter = transformEmailRecipientFilter(newsletter, emailRecipientFilter, 'email_segment');
const startRetrieve = Date.now();
debug('addEmail: retrieving members count');
@ -423,11 +416,8 @@ async function getEmailMemberRows({emailModel, memberSegment, options}) {
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
const filterOptions = Object.assign({}, knexOptions);
let newsletter = null;
if (labsService.isSet('multipleNewsletters')) {
newsletter = await emailModel.related('newsletter').fetch(Object.assign({}, {require: false}, _.pick(options, ['transacting'])));
}
const recipientFilter = transformEmailRecipientFilter(emailModel.get('recipient_filter'), {errorProperty: 'recipient_filter'}, newsletter);
const newsletter = emailModel.relations.newsletter ?? await emailModel.related('newsletter').fetch(Object.assign({}, {require: true}, _.pick(options, ['transacting'])));
const recipientFilter = transformEmailRecipientFilter(newsletter, emailModel.get('recipient_filter'), 'recipient_filter');
filterOptions.filter = recipientFilter;
if (memberSegment) {

View File

@ -3,9 +3,8 @@ const {BadRequestError} = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const messages = {
invalidEmailRecipientFilter: 'Invalid filter in email_recipient_filter param.',
invalidVisibilityFilter: 'Invalid visibility filter.',
invalidNewsletterId: 'The newsletter_id parameter doesn\'t match any active newsletter.'
invalidEmailSegment: 'The email segment parameter doesn\'t contain a valid filter'
};
class PostsService {
@ -17,69 +16,26 @@ class PostsService {
}
async editPost(frame) {
let model;
// Make sure the newsletter_id is matching an active newsletter
if (frame.options.newsletter_id) {
const newsletter = await this.models.Newsletter.findOne({id: frame.options.newsletter_id, filter: 'status:active'}, {transacting: frame.options.transacting});
if (!newsletter) {
throw new BadRequestError({
message: messages.invalidNewsletterId
});
}
} else {
// Set the newsletter_id if it isn't passed to the API
// NOTE: this option is ignored if the newsletter_id is already set on the post.
// Never use frame.options.newsletter_id to do actual logic. Use model.newsletter_id after the edit.
const newsletters = await this.models.Newsletter.findPage({filter: 'status:active', limit: 1, columns: ['id']}, {transacting: frame.options.transacting});
if (newsletters.data.length > 0) {
frame.options.newsletter_id = newsletters.data[0].id;
}
}
if (!frame.options.email_recipient_filter && frame.options.send_email_when_published) {
await this.models.Base.transaction(async (transacting) => {
const options = {
...frame.options,
transacting
};
/**
* 1. We need to edit the post first in order to know what the visibility is.
* 2. We can only pass the email_recipient_filter when we change the status.
*
* So, we first edit the post as requested, with all information except the status,
* from there we can determine what the email_recipient_filter should be and then finish
* the edit, with the status and the email_recipient_filter option.
*/
const status = frame.data.posts[0].status;
delete frame.data.posts[0].status;
const interimModel = await this.models.Post.edit(frame.data.posts[0], options);
frame.data.posts[0].status = status;
options.email_recipient_filter = interimModel.get('visibility') === 'paid' ? 'paid' : 'all';
model = await this.models.Post.edit(frame.data.posts[0], options);
});
} else {
model = await this.models.Post.edit(frame.data.posts[0], frame.options);
}
/**Handle newsletter email */
const emailRecipientFilter = model.get('email_recipient_filter');
if (emailRecipientFilter !== 'none') {
if (emailRecipientFilter !== 'all') {
// Make sure the newsletter is matching an active newsletter
// Note that this option is simply ignored if the post isn't published or scheduled
if (frame.options.newsletter && frame.options.email_segment) {
if (frame.options.email_segment !== 'all') {
// check filter is valid
try {
await this.models.Member.findPage({filter: emailRecipientFilter, limit: 1});
await this.models.Member.findPage({filter: frame.options.email_segment, limit: 1});
} catch (err) {
return Promise.reject(new BadRequestError({
message: tpl(messages.invalidEmailRecipientFilter),
message: tpl(messages.invalidEmailSegment),
context: err.message
}));
}
}
}
const model = await this.models.Post.edit(frame.data.posts[0], frame.options);
/**Handle newsletter email */
if (model.get('newsletter_id')) {
const sendEmail = model.wasChanged() && this.shouldSendEmail(model.get('status'), model.previous('status'));
if (sendEmail) {

View File

@ -436,7 +436,7 @@ exports[`Members API - With Newsletters - compat mode Can fetch members who are
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "11136",
"content-length": "11493",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -662,7 +662,7 @@ exports[`Members API - With Newsletters Can fetch members who are subscribed 2:
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "11136",
"content-length": "11493",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",

View File

@ -187,7 +187,7 @@ exports[`Members API Can add a subscription 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2066",
"content-length": "2117",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -266,7 +266,7 @@ exports[`Members API Can add a subscription 4: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2066",
"content-length": "2117",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -296,7 +296,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Weekly newsletter",
"sender_email": "jamie@example.com",
@ -335,7 +335,7 @@ exports[`Members API Can add and edit with custom newsletters 2: [headers] 1`] =
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1305",
"content-length": "1356",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
@ -366,7 +366,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Daily newsletter",
"sender_email": "jamie@example.com",
@ -405,7 +405,7 @@ exports[`Members API Can add and edit with custom newsletters 4: [headers] 1`] =
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1304",
"content-length": "1355",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -461,7 +461,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Weekly newsletter",
"sender_email": "jamie@example.com",
@ -500,7 +500,7 @@ exports[`Members API Can add and send a signup confirmation email (old) 2: [head
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1800",
"content-length": "1851",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": Any<String>,
@ -567,7 +567,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Daily newsletter",
"sender_email": "jamie@example.com",
@ -606,7 +606,7 @@ exports[`Members API Can add and send a signup confirmation email 2: [headers] 1
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1795",
"content-length": "1846",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": Any<String>,
@ -899,7 +899,7 @@ exports[`Members API Can browse 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "13097",
"content-length": "13454",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -949,7 +949,7 @@ exports[`Members API Can browse with filter 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1328",
"content-length": "1379",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -999,7 +999,7 @@ exports[`Members API Can browse with search 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1328",
"content-length": "1379",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -1291,7 +1291,7 @@ exports[`Members API Can destroy 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1770",
"content-length": "1821",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
@ -1604,7 +1604,7 @@ exports[`Members API Can filter by paid status 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "9569",
"content-length": "9773",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -1717,7 +1717,7 @@ exports[`Members API Can filter on newsletter slug 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "8285",
"content-length": "8540",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -1901,7 +1901,7 @@ exports[`Members API Can ignore any unknown includes 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "9569",
"content-length": "9773",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -2337,7 +2337,7 @@ exports[`Members API Can read 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1265",
"content-length": "1316",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -2380,7 +2380,7 @@ exports[`Members API Can read and include email_recipients 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1287",
"content-length": "1338",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -2422,7 +2422,7 @@ exports[`Members API Can read and include tiers 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1265",
"content-length": "1316",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -2521,7 +2521,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Weekly newsletter",
"sender_email": "jamie@example.com",
@ -2560,7 +2560,7 @@ exports[`Members API Can subscribe by setting (old) subscribed property to true
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1784",
"content-length": "1835",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -2645,7 +2645,7 @@ exports[`Members API Can subscribe to a newsletter 4: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1127",
"content-length": "1178",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -2775,7 +2775,7 @@ exports[`Members API Can subscribe to a newsletter 5: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "4326",
"content-length": "4377",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -3039,7 +3039,7 @@ exports[`Members API Search by case-insensitive email MEMBER2 receives member wi
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1175",
"content-length": "1226",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -3089,7 +3089,7 @@ exports[`Members API Search by case-insensitive name egg receives member with na
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1328",
"content-length": "1379",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -3167,7 +3167,7 @@ exports[`Members API Search for paid members retrieves member with email paid@te
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2384",
"content-length": "2435",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -3209,7 +3209,7 @@ exports[`Members API Subscribes to default newsletters 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1771",
"content-length": "1822",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,

View File

@ -392,7 +392,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Daily newsletter",
"sender_email": "jamie@example.com",
@ -418,7 +418,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Weekly newsletter",
"sender_email": "jamie@example.com",
@ -444,7 +444,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Old newsletter",
"sender_email": "jamie@example.com",
@ -473,7 +473,7 @@ exports[`Newsletters API Can browse newsletters 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2690",
"content-length": "2843",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -494,7 +494,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Updated newsletter name",
"sender_email": "jamie@example.com",
@ -523,7 +523,7 @@ exports[`Newsletters API Can edit a newsletters and update the sender_email when
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "724",
"content-length": "775",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -540,7 +540,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Updated newsletter name",
"sender_email": "jamie@example.com",
@ -569,7 +569,7 @@ exports[`Newsletters API Can edit newsletters 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "672",
"content-length": "723",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -681,7 +681,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Daily newsletter",
"sender_email": "jamie@example.com",
@ -711,7 +711,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Weekly newsletter",
"sender_email": "jamie@example.com",
@ -741,7 +741,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Old newsletter",
"sender_email": "jamie@example.com",
@ -770,7 +770,7 @@ exports[`Newsletters API Can include members & posts counts when browsing newsle
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2818",
"content-length": "2971",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -790,7 +790,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Updated newsletter name 2",
"sender_email": "jamie@example.com",
@ -819,7 +819,7 @@ exports[`Newsletters API Can include members & posts counts when editing newslet
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "706",
"content-length": "757",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -840,7 +840,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Daily newsletter",
"sender_email": "jamie@example.com",
@ -869,7 +869,7 @@ exports[`Newsletters API Can include members & posts counts when reading a newsl
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "697",
"content-length": "748",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -885,7 +885,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Daily newsletter",
"sender_email": "jamie@example.com",
@ -914,7 +914,7 @@ exports[`Newsletters API Can read a newsletter 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "665",
"content-length": "716",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -930,7 +930,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Updated newsletter name",
"sender_email": "verify@example.com",
@ -1310,7 +1310,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Updated active newsletter name",
"sender_email": "jamie@example.com",
@ -1339,7 +1339,7 @@ exports[`Newsletters API Host Settings: newsletter limits Max limit Editing an a
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "678",
"content-length": "729",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -1356,7 +1356,7 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"footer_content": null,
"header_image": null,
"header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Updated archived newsletter name",
"sender_email": "jamie@example.com",
@ -1385,7 +1385,7 @@ exports[`Newsletters API Host Settings: newsletter limits Max limit Editing an a
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "680",
"content-length": "731",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",

View File

@ -477,9 +477,9 @@ describe('Posts API', function () {
res2.body.posts[0].status.should.eql('draft');
});
it(`Can't change the newsletter_id of a post from the post body`, async function () {
it(`Can't change the newsletter of a post from the post body`, async function () {
const post = {
newsletter_id: testUtils.DataGenerator.Content.newsletters[0].id
newsletter: testUtils.DataGenerator.Content.newsletters[0].id
};
const postId = testUtils.DataGenerator.Content.posts[2].id;
@ -588,8 +588,53 @@ describe('Posts API', function () {
should(model.get('newsletter_id')).eql(null);
});
it('Can change the newsletter_id of a post when publishing', async function () {
const newsletterId = testUtils.DataGenerator.Content.newsletters[2].id;
it('Cannot send to an archived newsletter', async function () {
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[2].slug;
should(testUtils.DataGenerator.Content.newsletters[2].status).eql('archived', 'This test expects an archived newsletter in the test fixtures');
const post = {
title: 'My archived newsletter post',
status: 'draft',
feature_image_alt: 'Testing newsletter',
feature_image_caption: 'Testing <b>feature image caption</b>',
mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('my post'),
created_at: moment().subtract(2, 'days').toDate(),
updated_at: moment().subtract(2, 'days').toDate(),
created_by: ObjectId().toHexString(),
updated_by: ObjectId().toHexString()
};
const res = await request.post(localUtils.API.getApiQuery('posts'))
.set('Origin', config.get('url'))
.send({posts: [post]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
// Check newsletter relation is loaded, but null in response.
should(res.body.posts[0].newsletter).eql(null);
should(res.body.posts[0].email_segment).eql('all');
should.not.exist(res.body.posts[0].newsletter_id);
const id = res.body.posts[0].id;
const updatedPost = res.body.posts[0];
updatedPost.status = 'published';
await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(400);
});
it('Can change the newsletter of a post when publishing', async function () {
const newsletterId = testUtils.DataGenerator.Content.newsletters[1].id;
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[1].slug;
const post = {
title: 'My newsletter_id post',
@ -621,7 +666,7 @@ describe('Posts API', function () {
updatedPost.status = 'published';
const finalPost = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_recipient_filter=all&newsletter_id=' + newsletterId))
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
@ -630,6 +675,7 @@ describe('Posts API', function () {
// Check newsletter relation is loaded in response
should(finalPost.body.posts[0].newsletter.id).eql(newsletterId);
should(finalPost.body.posts[0].email_segment).eql('all');
should.not.exist(finalPost.body.posts[0].newsletter_id);
const model = await models.Post.findOne({
@ -649,8 +695,139 @@ describe('Posts API', function () {
should(email.get('status')).eql('pending');
});
it('Can publish an email_only post', async function () {
const newsletterId = testUtils.DataGenerator.Content.newsletters[1].id;
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[1].slug;
const post = {
title: 'My post',
status: 'draft',
feature_image_alt: 'Testing newsletter in posts',
feature_image_caption: 'Testing <b>feature image caption</b>',
mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('my post'),
created_at: moment().subtract(2, 'days').toDate(),
updated_at: moment().subtract(2, 'days').toDate(),
created_by: ObjectId().toHexString(),
updated_by: ObjectId().toHexString()
};
const res = await request.post(localUtils.API.getApiQuery('posts'))
.set('Origin', config.get('url'))
.send({posts: [post]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
const id = res.body.posts[0].id;
const updatedPost = res.body.posts[0];
updatedPost.status = 'published';
//updatedPost.published_at = moment().add(2, 'days').toDate();
updatedPost.email_only = true;
const publishedRes = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
const publishedPost = publishedRes.body.posts[0];
publishedPost.newsletter.id.should.eql(newsletterId);
publishedPost.email_segment.should.eql('all');
publishedPost.status.should.eql('sent');
should.not.exist(publishedPost.newsletter_id);
let model = await models.Post.findOne({
id,
status: 'all'
}, testUtils.context.internal);
should(model.get('status')).eql('sent');
should(model.get('newsletter_id')).eql(newsletterId);
should(model.get('email_recipient_filter')).eql('all');
// We should have an email
const email = await models.Email.findOne({
post_id: id
}, testUtils.context.internal);
should(email.get('newsletter_id')).eql(newsletterId);
should(email.get('recipient_filter')).eql('all');
should(email.get('status')).eql('pending');
});
it('Can publish an email_only post with free filter', async function () {
const newsletterId = testUtils.DataGenerator.Content.newsletters[1].id;
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[1].slug;
const post = {
title: 'My post',
status: 'draft',
feature_image_alt: 'Testing newsletter in posts',
feature_image_caption: 'Testing <b>feature image caption</b>',
mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('my post'),
created_at: moment().subtract(2, 'days').toDate(),
updated_at: moment().subtract(2, 'days').toDate(),
created_by: ObjectId().toHexString(),
updated_by: ObjectId().toHexString()
};
const res = await request.post(localUtils.API.getApiQuery('posts'))
.set('Origin', config.get('url'))
.send({posts: [post]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
const id = res.body.posts[0].id;
const updatedPost = res.body.posts[0];
updatedPost.status = 'published';
//updatedPost.published_at = moment().add(2, 'days').toDate();
updatedPost.email_only = true;
const publishedRes = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug + '&email_segment=status%3Afree'))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
const publishedPost = publishedRes.body.posts[0];
publishedPost.newsletter.id.should.eql(newsletterId);
publishedPost.email_segment.should.eql('status:free');
publishedPost.status.should.eql('sent');
should.not.exist(publishedPost.newsletter_id);
let model = await models.Post.findOne({
id,
status: 'all'
}, testUtils.context.internal);
should(model.get('status')).eql('sent');
should(model.get('newsletter_id')).eql(newsletterId);
should(model.get('email_recipient_filter')).eql('status:free');
// We should have an email
const email = await models.Email.findOne({
post_id: id
}, testUtils.context.internal);
should(email.get('newsletter_id')).eql(newsletterId);
should(email.get('recipient_filter')).eql('status:free');
should(email.get('status')).eql('pending');
});
it('Can publish a scheduled post', async function () {
const newsletterId = testUtils.DataGenerator.Content.newsletters[2].id;
const newsletterId = testUtils.DataGenerator.Content.newsletters[1].id;
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[1].slug;
const post = {
title: 'My scheduled post',
@ -679,7 +856,7 @@ describe('Posts API', function () {
updatedPost.published_at = moment().add(2, 'days').toDate();
const scheduledRes = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_recipient_filter=all&newsletter_id=' + newsletterId))
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
@ -689,6 +866,7 @@ describe('Posts API', function () {
const scheduledPost = scheduledRes.body.posts[0];
scheduledPost.newsletter.id.should.eql(newsletterId);
scheduledPost.email_segment.should.eql('all');
should.not.exist(scheduledPost.newsletter_id);
let model = await models.Post.findOne({
@ -697,6 +875,7 @@ describe('Posts API', function () {
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(newsletterId);
should(model.get('email_recipient_filter')).eql('all');
// We should not have an email
let email = await models.Email.findOne({
@ -724,6 +903,7 @@ describe('Posts API', function () {
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(newsletterId);
should(model.get('email_recipient_filter')).eql('all');
publishedPost.newsletter.id.should.eql(newsletterId);
should.not.exist(publishedPost.newsletter_id);
@ -734,16 +914,18 @@ describe('Posts API', function () {
}, testUtils.context.internal);
should(email.get('newsletter_id')).eql(newsletterId);
should(email.get('recipient_filter')).eql('all');
should(email.get('status')).eql('pending');
});
it('Defaults to the default newsletter when publishing without a newsletter_id', async function () {
const defaultNewsletter = await models.Newsletter.getDefaultNewsletter();
it('Can publish a scheduled post with custom email segment', async function () {
const newsletterId = testUtils.DataGenerator.Content.newsletters[1].id;
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[1].slug;
const post = {
title: 'My post without newsletter_id',
title: 'My scheduled post 2',
status: 'draft',
feature_image_alt: 'Testing newsletter_id',
feature_image_alt: 'Testing newsletter_id in scheduled posts',
feature_image_caption: 'Testing <b>feature image caption</b>',
mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('my post'),
created_at: moment().subtract(2, 'days').toDate(),
@ -761,32 +943,263 @@ describe('Posts API', function () {
const id = res.body.posts[0].id;
const updatedPost = {
status: 'published',
updated_at: res.body.posts[0].updated_at
};
const updatedPost = res.body.posts[0];
const finalPost = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_recipient_filter=all'))
updatedPost.status = 'scheduled';
updatedPost.published_at = moment().add(2, 'days').toDate();
const scheduledRes = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug + '&email_segment=status:free'))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
// Check newsletter relation is loaded in response
should(finalPost.body.posts[0].newsletter.id).eql(defaultNewsletter.get('id'));
should.not.exist(finalPost.body.posts[0].newsletter_id);
const scheduledPost = scheduledRes.body.posts[0];
scheduledPost.newsletter.id.should.eql(newsletterId);
scheduledPost.email_segment.should.eql('status:free');
should.not.exist(scheduledPost.newsletter_id);
const model = await models.Post.findOne({
let model = await models.Post.findOne({
id,
status: 'scheduled'
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(newsletterId);
should(model.get('email_recipient_filter')).eql('status:free');
// We should not have an email
let email = await models.Email.findOne({
post_id: id
}, testUtils.context.internal);
should.not.exist(email);
// Publish now, without passing the newsletter_id or other options again!
scheduledPost.status = 'published';
scheduledPost.published_at = moment().toDate();
const publishedRes = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/'))
.set('Origin', config.get('url'))
.send({posts: [scheduledPost]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
const publishedPost = publishedRes.body.posts[0];
model = await models.Post.findOne({
id
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(defaultNewsletter.get('id'));
should(model.get('newsletter_id')).eql(newsletterId);
should(model.get('email_recipient_filter')).eql('status:free');
publishedPost.newsletter.id.should.eql(newsletterId);
should.not.exist(publishedPost.newsletter_id);
// Check email is sent to the correct newsletter
email = await models.Email.findOne({
post_id: id
}, testUtils.context.internal);
should(email.get('newsletter_id')).eql(newsletterId);
should(email.get('recipient_filter')).eql('status:free');
should(email.get('status')).eql('pending');
});
it('Can\'t change the newsletter_id once it has been set', async function () {
// Note: this test only works if there are members subscribed to the initial newsletter
it('Can publish a scheduled post without newsletter', async function () {
const post = {
title: 'My scheduled post 3',
status: 'draft',
feature_image_alt: 'Testing no newsletter in scheduled posts',
feature_image_caption: 'Testing <b>feature image caption</b>',
mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('my post'),
created_at: moment().subtract(2, 'days').toDate(),
updated_at: moment().subtract(2, 'days').toDate(),
created_by: ObjectId().toHexString(),
updated_by: ObjectId().toHexString()
};
const res = await request.post(localUtils.API.getApiQuery('posts'))
.set('Origin', config.get('url'))
.send({posts: [post]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
const id = res.body.posts[0].id;
const updatedPost = res.body.posts[0];
updatedPost.status = 'scheduled';
updatedPost.published_at = moment().add(2, 'days').toDate();
const scheduledRes = await request
// We also test whether email_segment is ignored if no newsletter is sent
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_segment=status:-free'))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
const scheduledPost = scheduledRes.body.posts[0];
should(scheduledPost.newsletter).eql(null);
scheduledPost.email_segment.should.eql('all'); // should be igored
should.not.exist(scheduledPost.newsletter_id);
let model = await models.Post.findOne({
id,
status: 'scheduled'
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(null);
should(model.get('email_recipient_filter')).eql('all');
// We should not have an email
let email = await models.Email.findOne({
post_id: id
}, testUtils.context.internal);
should.not.exist(email);
// Publish now, without passing the newsletter_id or other options again!
scheduledPost.status = 'published';
scheduledPost.published_at = moment().toDate();
const publishedRes = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/'))
.set('Origin', config.get('url'))
.send({posts: [scheduledPost]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
const publishedPost = publishedRes.body.posts[0];
model = await models.Post.findOne({
id
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(null);
should(model.get('email_recipient_filter')).eql('all');
should(publishedPost.newsletter).eql(null);
should.not.exist(publishedPost.newsletter_id);
// Check email is sent to the correct newsletter
email = await models.Email.findOne({
post_id: id
}, testUtils.context.internal);
should.not.exist(email);
});
it('Can publish a scheduled email only post', async function () {
const newsletterId = testUtils.DataGenerator.Content.newsletters[1].id;
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[1].slug;
const post = {
title: 'My scheduled email only post',
status: 'draft',
feature_image_alt: 'Testing newsletter in scheduled posts',
feature_image_caption: 'Testing <b>feature image caption</b>',
mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('my post'),
created_at: moment().subtract(2, 'days').toDate(),
updated_at: moment().subtract(2, 'days').toDate(),
created_by: ObjectId().toHexString(),
updated_by: ObjectId().toHexString()
};
const res = await request.post(localUtils.API.getApiQuery('posts'))
.set('Origin', config.get('url'))
.send({posts: [post]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
const id = res.body.posts[0].id;
const updatedPost = res.body.posts[0];
updatedPost.status = 'scheduled';
updatedPost.email_only = true;
updatedPost.published_at = moment().add(2, 'days').toDate();
const scheduledRes = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
const scheduledPost = scheduledRes.body.posts[0];
scheduledPost.newsletter.id.should.eql(newsletterId);
scheduledPost.email_segment.should.eql('all');
scheduledPost.status.should.eql('scheduled');
scheduledPost.email_only.should.eql(true);
should.not.exist(scheduledPost.newsletter_id);
let model = await models.Post.findOne({
id,
status: 'scheduled'
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(newsletterId);
should(model.get('status')).eql('scheduled');
should(model.get('email_recipient_filter')).eql('all');
// We should not have an email
let email = await models.Email.findOne({
post_id: id
}, testUtils.context.internal);
should.not.exist(email);
// Publish now, without passing the newsletter_id or other options again!
scheduledPost.status = 'published';
const publishedRes = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/'))
.set('Origin', config.get('url'))
.send({posts: [scheduledPost]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
const publishedPost = publishedRes.body.posts[0];
model = await models.Post.findOne({
id,
status: 'all'
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(newsletterId);
should(model.get('status')).eql('sent');
should(model.get('email_recipient_filter')).eql('all');
publishedPost.newsletter.id.should.eql(newsletterId);
should.not.exist(publishedPost.newsletter_id);
// Check email is sent to the correct newsletter
email = await models.Email.findOne({
post_id: id
}, testUtils.context.internal);
should(email.get('newsletter_id')).eql(newsletterId);
should(email.get('recipient_filter')).eql('all');
should(email.get('status')).eql('pending');
});
it('Can\'t change the newsletter once it has been sent', async function () {
// Note: this test only works if there are members subscribed to the initial newsletter
// (so it won't get reset when changing the post status to draft again)
let model;
@ -811,7 +1224,9 @@ describe('Posts API', function () {
const id = res.body.posts[0].id;
const newsletterId = testUtils.DataGenerator.Content.newsletters[0].id;
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[0].slug;
const newsletterId2 = testUtils.DataGenerator.Content.newsletters[1].id;
const newsletterSlug2 = testUtils.DataGenerator.Content.newsletters[1].slug;
const updatedPost = {
status: 'published',
@ -819,7 +1234,7 @@ describe('Posts API', function () {
};
const res2 = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_recipient_filter=status:-free&send_email_when_published=true&newsletter_id=' + newsletterId))
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_segment=status:-free&newsletter=' + newsletterSlug))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
@ -828,6 +1243,8 @@ describe('Posts API', function () {
// Check newsletter relation is loaded in response
should(res2.body.posts[0].newsletter.id).eql(newsletterId);
should(res2.body.posts[0].email_segment).eql('status:-free');
should.not.exist(res2.body.posts[0].newsletter_id);
model = await models.Post.findOne({
@ -835,6 +1252,7 @@ describe('Posts API', function () {
status: 'published'
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(newsletterId);
should(model.get('email_recipient_filter')).eql('status:-free');
// Check email is sent to the correct newsletter
let email = await models.Email.findOne({
@ -842,6 +1260,7 @@ describe('Posts API', function () {
}, testUtils.context.internal);
should(email.get('newsletter_id')).eql(newsletterId);
should(email.get('recipient_filter')).eql('status:-free');
should(email.get('status')).eql('pending');
const unpublished = {
@ -858,7 +1277,9 @@ describe('Posts API', function () {
.expect(200);
// Check newsletter relation is loaded in response
// We should keep it, because we already sent an email
should(res3.body.posts[0].newsletter.id).eql(newsletterId);
should(res2.body.posts[0].email_segment).eql('status:-free');
should.not.exist(res3.body.posts[0].newsletter_id);
model = await models.Post.findOne({
@ -883,7 +1304,7 @@ describe('Posts API', function () {
};
const res4 = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_recipient_filter=all&newsletter_id=' + newsletterId2))
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug2))
.set('Origin', config.get('url'))
.send({posts: [republished]})
.expect('Content-Type', /json/)
@ -893,6 +1314,7 @@ describe('Posts API', function () {
// Check newsletter relation is loaded in response
// + did update the newsletter id
should(res4.body.posts[0].newsletter.id).eql(newsletterId);
should(res4.body.posts[0].email_segment).eql('status:-free');
should.not.exist(res4.body.posts[0].newsletter_id);
model = await models.Post.findOne({
@ -903,7 +1325,7 @@ describe('Posts API', function () {
// Should not change if status remains published
const res5 = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_recipient_filter=all&newsletter_id=' + newsletterId))
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug))
.set('Origin', config.get('url'))
.send({posts: [republished]})
.expect('Content-Type', /json/)
@ -913,6 +1335,7 @@ describe('Posts API', function () {
// Check newsletter relation is loaded in response
// + did not update the newsletter id
should(res5.body.posts[0].newsletter.id).eql(newsletterId);
should(res5.body.posts[0].email_segment).eql('status:-free');
should.not.exist(res5.body.posts[0].newsletter_id);
model = await models.Post.findOne({
@ -924,6 +1347,160 @@ describe('Posts API', function () {
should(model.get('newsletter_id')).eql(newsletterId);
});
it('Can change the newsletter if it has not been sent', async function () {
// Note: this test only works if there are NO members subscribed to the initial newsletter
// (so it will get reset when changing the post status to draft again)
let model;
const post = {
title: 'My post that will get a changed newsletter',
status: 'draft',
feature_image_alt: 'Testing newsletter',
feature_image_caption: 'Testing <b>feature image caption</b>',
mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('my post'),
created_at: moment().subtract(2, 'days').toDate(),
updated_at: moment().subtract(2, 'days').toDate(),
created_by: ObjectId().toHexString(),
updated_by: ObjectId().toHexString()
};
const res = await request.post(localUtils.API.getApiQuery('posts'))
.set('Origin', config.get('url'))
.send({posts: [post]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
const id = res.body.posts[0].id;
// Check default values
should(res.body.posts[0].newsletter).eql(null);
should(res.body.posts[0].email_segment).eql('all');
const newsletterId = testUtils.DataGenerator.Content.newsletters[0].id;
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[0].slug;
const newsletterId2 = testUtils.DataGenerator.Content.newsletters[1].id;
const newsletterSlug2 = testUtils.DataGenerator.Content.newsletters[1].slug;
const updatedPost = {
status: 'published',
updated_at: res.body.posts[0].updated_at
};
const res2 = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_segment=id:0&newsletter=' + newsletterSlug))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
// Check newsletter relation is loaded in response
should(res2.body.posts[0].newsletter.id).eql(newsletterId);
should(res2.body.posts[0].email_segment).eql('id:0');
should.not.exist(res2.body.posts[0].newsletter_id);
model = await models.Post.findOne({
id: id,
status: 'published'
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(newsletterId);
should(model.get('email_recipient_filter')).eql('id:0');
// Check email is sent to the correct newsletter
let email = await models.Email.findOne({
post_id: id
}, testUtils.context.internal);
should(email).eql(null);
const unpublished = {
status: 'draft',
updated_at: res2.body.posts[0].updated_at
};
const res3 = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/'))
.set('Origin', config.get('url'))
.send({posts: [unpublished]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
// Check is reset
should(res3.body.posts[0].newsletter).eql(null);
should.not.exist(res3.body.posts[0].newsletter_id);
should(res3.body.posts[0].email_segment).eql('all');
model = await models.Post.findOne({
id: id,
status: 'draft'
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(null);
should(model.get('email_recipient_filter')).eql('all');
const republished = {
status: 'published',
updated_at: res3.body.posts[0].updated_at
};
const res4 = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_segment=status:-free&newsletter=' + newsletterSlug2))
.set('Origin', config.get('url'))
.send({posts: [republished]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
// Check newsletter relation is loaded in response
// + did update the newsletter id
should(res4.body.posts[0].newsletter.id).eql(newsletterId2);
should(res4.body.posts[0].email_segment).eql('status:-free');
should.not.exist(res4.body.posts[0].newsletter_id);
model = await models.Post.findOne({
id: id,
status: 'published'
}, testUtils.context.internal);
should(model.get('newsletter_id')).eql(newsletterId2);
should(model.get('email_recipient_filter')).eql('status:-free');
// Check email is sent to the correct newsletter
email = await models.Email.findOne({
post_id: id
}, testUtils.context.internal);
should(email.get('newsletter_id')).eql(newsletterId2);
should(email.get('recipient_filter')).eql('status:-free');
should(email.get('status')).eql('pending');
// Should not change if status remains published
const res5 = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug))
.set('Origin', config.get('url'))
.send({posts: [republished]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
// Check newsletter relation is loaded in response
// + did not update the newsletter id
should(res5.body.posts[0].newsletter.id).eql(newsletterId2);
should(res5.body.posts[0].email_segment).eql('status:-free');
should.not.exist(res5.body.posts[0].newsletter_id);
model = await models.Post.findOne({
id: id,
status: 'published'
}, testUtils.context.internal);
// Test if the newsletter_id option was ignored
should(model.get('newsletter_id')).eql(newsletterId2);
should(model.get('email_recipient_filter')).eql('status:-free');
});
it('Can destroy a post', async function () {
const res = await request
.del(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
@ -971,7 +1548,8 @@ describe('Posts API', function () {
});
it('Can publish a post and send as email', async function () {
const newsletterId = testUtils.DataGenerator.Content.newsletters[2].id;
const newsletterId = testUtils.DataGenerator.Content.newsletters[1].id;
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[1].slug;
const post = {
title: 'Author newsletter_id post',
@ -1004,7 +1582,7 @@ describe('Posts API', function () {
updatedPost.status = 'published';
const finalPost = await request
.put(localUtils.API.getApiQuery('posts/' + id + '/?email_recipient_filter=all&newsletter_id=' + newsletterId))
.put(localUtils.API.getApiQuery('posts/' + id + '/?newsletter=' + newsletterSlug))
.set('Origin', config.get('url'))
.send({posts: [updatedPost]})
.expect('Content-Type', /json/)
@ -1013,6 +1591,7 @@ describe('Posts API', function () {
// Check newsletter relation is loaded in response
should(finalPost.body.posts[0].newsletter.id).eql(newsletterId);
should(finalPost.body.posts[0].email_segment).eql('all');
should.not.exist(finalPost.body.posts[0].newsletter_id);
const model = await models.Post.findOne({
@ -1049,7 +1628,7 @@ describe('Posts API', function () {
// NOTE: need to do a full reboot to reinitialize hostSettings
await localUtils.startGhost();
request = supertest.agent(config.get('url'));
await localUtils.doAuth(request, 'users:extra', 'posts', 'emails');
await localUtils.doAuth(request, 'users:extra', 'posts', 'emails', 'newsletters');
const draftPostResponse = await request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[3].id}/`))
@ -1058,8 +1637,11 @@ describe('Posts API', function () {
const draftPost = draftPostResponse.body.posts[0];
const newsletterId = testUtils.DataGenerator.Content.newsletters[1].id;
const newsletterSlug = testUtils.DataGenerator.Content.newsletters[1].slug;
const response = await request
.put(localUtils.API.getApiQuery(`posts/${draftPost.id}/?email_recipient_filter=all&send_email_when_published=true`))
.put(localUtils.API.getApiQuery(`posts/${draftPost.id}/?newsletter=${newsletterSlug}`))
.set('Origin', config.get('url'))
.send({posts: [{
status: 'published',

View File

@ -43,7 +43,7 @@ const expectedProperties = {
'featured',
'status',
'visibility',
'email_recipient_filter',
'email_segment',
'created_at',
'updated_at',
'published_at',

View File

@ -22,7 +22,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"feature_image": "https://static.ghost.org/v4.0.0/images/welcome-to-ghost.png",
@ -57,7 +56,7 @@ exports[`Posts Content API Can filter by published date 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "4591",
"content-length": "4559",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",
@ -87,7 +86,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "How to tweak a few settings in Ghost to transform your site from a generic template to a custom brand with style and personality.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "How to tweak a few settings in Ghost to transform your site from a generic template to a custom brand with style and personality.",
"feature_image": "https://static.ghost.org/v4.0.0/images/publishing-options.png",
@ -134,7 +132,7 @@ exports[`Posts Content API Can filter by published date 4: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "5882",
"content-length": "5850",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",
@ -164,7 +162,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"feature_image": "https://static.ghost.org/v4.0.0/images/welcome-to-ghost.png",
@ -199,7 +196,7 @@ exports[`Posts Content API Can filter by published date 6: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "4592",
"content-length": "4560",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",
@ -261,7 +258,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": " * Lorem
* Aliquam [http://127.0.0.1:2369/about#nowhere]
@ -343,7 +339,6 @@ mi vitae est. Mauris placerat eleifend leo.
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"feature_image": "https://static.ghost.org/v4.0.0/images/welcome-to-ghost.png",
@ -411,7 +406,6 @@ mi vitae est. Mauris placerat eleifend leo.
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "How to tweak a few settings in Ghost to transform your site from a generic template to a custom brand with style and personality.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "How to tweak a few settings in Ghost to transform your site from a generic template to a custom brand with style and personality.",
"feature_image": "https://static.ghost.org/v4.0.0/images/publishing-options.png",
@ -491,7 +485,6 @@ mi vitae est. Mauris placerat eleifend leo.
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "A full overview of all the features built into the Ghost editor, including powerful workflow automations to speed up your creative process.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "A full overview of all the features built into the Ghost editor, including powerful workflow automations to speed up your creative process.",
"feature_image": "https://static.ghost.org/v4.0.0/images/writing-posts-with-ghost.png",
@ -559,7 +552,6 @@ mi vitae est. Mauris placerat eleifend leo.
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "How Ghost allows you to turn anonymous readers into an audience of active subscribers, so you know what's working and what isn't.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "How Ghost allows you to turn anonymous readers into an audience of active subscribers, so you know what's working and what isn't.",
"feature_image": "https://static.ghost.org/v4.0.0/images/creating-a-custom-theme.png",
@ -627,7 +619,6 @@ mi vitae est. Mauris placerat eleifend leo.
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "For creators and aspiring entrepreneurs looking to generate a sustainable
recurring revenue stream from their creative work, Ghost has built-in payments
@ -726,7 +717,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "A guide to collaborating with other staff users to publish, and some resources to help you with the next steps of growing your business",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "A guide to collaborating with other staff users to publish, and some resources to help you with the next steps of growing your business",
"feature_image": "https://static.ghost.org/v4.0.0/images/admin-settings.png",
@ -794,7 +784,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "Work with all your favorite apps and tools or create your own custom integrations using the Ghost API.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "Work with all your favorite apps and tools or create your own custom integrations using the Ghost API.",
"feature_image": "https://static.ghost.org/v4.0.0/images/app-integrations.png",
@ -863,7 +852,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "testing
mctesters
@ -943,7 +931,6 @@ mctesters
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "HTML Ipsum Presents
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac
@ -1018,7 +1005,6 @@ ipsum rutrum or",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "This is my custom excerpt!",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "This is my custom excerpt!",
"feature_image": "https://example.com/super_photo.jpg",
@ -1068,7 +1054,7 @@ exports[`Posts Content API Can filter posts by authors 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "55261",
"content-length": "54909",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",
@ -1098,7 +1084,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": " * Lorem
* Aliquam [http://127.0.0.1:2369/about#nowhere]
@ -1150,7 +1135,6 @@ mi vitae est. Mauris placerat eleifend leo.
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "testing
mctesters
@ -1242,7 +1226,6 @@ mctesters
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "HTML Ipsum Presents
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac
@ -1350,7 +1333,6 @@ ipsum rutrum or",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "This is my custom excerpt!",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "This is my custom excerpt!",
"feature_image": "https://example.com/super_photo.jpg",
@ -1450,7 +1432,7 @@ exports[`Posts Content API Can filter posts by tag 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "14037",
"content-length": "13909",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",
@ -1481,7 +1463,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"feature_image": "https://static.ghost.org/v4.0.0/images/welcome-to-ghost.png",
@ -1555,7 +1536,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "How to tweak a few settings in Ghost to transform your site from a generic template to a custom brand with style and personality.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "How to tweak a few settings in Ghost to transform your site from a generic template to a custom brand with style and personality.",
"feature_image": "https://static.ghost.org/v4.0.0/images/publishing-options.png",
@ -1641,7 +1621,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "A full overview of all the features built into the Ghost editor, including powerful workflow automations to speed up your creative process.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "A full overview of all the features built into the Ghost editor, including powerful workflow automations to speed up your creative process.",
"feature_image": "https://static.ghost.org/v4.0.0/images/writing-posts-with-ghost.png",
@ -1715,7 +1694,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "How Ghost allows you to turn anonymous readers into an audience of active subscribers, so you know what's working and what isn't.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "How Ghost allows you to turn anonymous readers into an audience of active subscribers, so you know what's working and what isn't.",
"feature_image": "https://static.ghost.org/v4.0.0/images/creating-a-custom-theme.png",
@ -1789,7 +1767,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "For creators and aspiring entrepreneurs looking to generate a sustainable
recurring revenue stream from their creative work, Ghost has built-in payments
@ -1894,7 +1871,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "A guide to collaborating with other staff users to publish, and some resources to help you with the next steps of growing your business",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "A guide to collaborating with other staff users to publish, and some resources to help you with the next steps of growing your business",
"feature_image": "https://static.ghost.org/v4.0.0/images/admin-settings.png",
@ -1968,7 +1944,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "Work with all your favorite apps and tools or create your own custom integrations using the Ghost API.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "Work with all your favorite apps and tools or create your own custom integrations using the Ghost API.",
"feature_image": "https://static.ghost.org/v4.0.0/images/app-integrations.png",
@ -2043,7 +2018,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": " * Lorem
* Aliquam [http://127.0.0.1:2369/about#nowhere]
@ -2111,7 +2085,6 @@ mi vitae est. Mauris placerat eleifend leo.
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "testing
mctesters
@ -2197,7 +2170,6 @@ mctesters
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "HTML Ipsum Presents
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac
@ -2278,7 +2250,6 @@ ipsum rutrum or",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "This is my custom excerpt!",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "This is my custom excerpt!",
"feature_image": "https://example.com/super_photo.jpg",
@ -2350,7 +2321,7 @@ exports[`Posts Content API Can include relations 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "65590",
"content-length": "65238",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",
@ -2370,7 +2341,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "This is my custom excerpt!",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "This is my custom excerpt!",
"feature_image": "https://example.com/super_photo.jpg",
@ -2405,7 +2375,7 @@ exports[`Posts Content API Can request a single post 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2377",
"content-length": "2345",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",
@ -2497,7 +2467,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"feature_image": "https://static.ghost.org/v4.0.0/images/welcome-to-ghost.png",
@ -2533,7 +2502,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "How to tweak a few settings in Ghost to transform your site from a generic template to a custom brand with style and personality.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "How to tweak a few settings in Ghost to transform your site from a generic template to a custom brand with style and personality.",
"feature_image": "https://static.ghost.org/v4.0.0/images/publishing-options.png",
@ -2581,7 +2549,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "A full overview of all the features built into the Ghost editor, including powerful workflow automations to speed up your creative process.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "A full overview of all the features built into the Ghost editor, including powerful workflow automations to speed up your creative process.",
"feature_image": "https://static.ghost.org/v4.0.0/images/writing-posts-with-ghost.png",
@ -2617,7 +2584,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "How Ghost allows you to turn anonymous readers into an audience of active subscribers, so you know what's working and what isn't.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "How Ghost allows you to turn anonymous readers into an audience of active subscribers, so you know what's working and what isn't.",
"feature_image": "https://static.ghost.org/v4.0.0/images/creating-a-custom-theme.png",
@ -2653,7 +2619,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "For creators and aspiring entrepreneurs looking to generate a sustainable
recurring revenue stream from their creative work, Ghost has built-in payments
@ -2720,7 +2685,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "A guide to collaborating with other staff users to publish, and some resources to help you with the next steps of growing your business",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "A guide to collaborating with other staff users to publish, and some resources to help you with the next steps of growing your business",
"feature_image": "https://static.ghost.org/v4.0.0/images/admin-settings.png",
@ -2756,7 +2720,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "Work with all your favorite apps and tools or create your own custom integrations using the Ghost API.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "Work with all your favorite apps and tools or create your own custom integrations using the Ghost API.",
"feature_image": "https://static.ghost.org/v4.0.0/images/app-integrations.png",
@ -2793,7 +2756,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": " * Lorem
* Aliquam [http://127.0.0.1:2369/about#nowhere]
@ -2843,7 +2805,6 @@ mi vitae est. Mauris placerat eleifend leo.
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "testing
mctesters
@ -2891,7 +2852,6 @@ mctesters
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "HTML Ipsum Presents
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac
@ -2934,7 +2894,6 @@ ipsum rutrum or",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "This is my custom excerpt!",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "This is my custom excerpt!",
"feature_image": "https://example.com/super_photo.jpg",
@ -2969,7 +2928,7 @@ exports[`Posts Content API Can request posts 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "46709",
"content-length": "46357",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",
@ -2999,7 +2958,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "We've crammed the most important information to help you get started with Ghost into this one post. It's your cheat-sheet to get started, and your shortcut to advanced features.",
"feature_image": "https://static.ghost.org/v4.0.0/images/welcome-to-ghost.png",
@ -3035,7 +2993,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "How to tweak a few settings in Ghost to transform your site from a generic template to a custom brand with style and personality.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "How to tweak a few settings in Ghost to transform your site from a generic template to a custom brand with style and personality.",
"feature_image": "https://static.ghost.org/v4.0.0/images/publishing-options.png",
@ -3083,7 +3040,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "A full overview of all the features built into the Ghost editor, including powerful workflow automations to speed up your creative process.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "A full overview of all the features built into the Ghost editor, including powerful workflow automations to speed up your creative process.",
"feature_image": "https://static.ghost.org/v4.0.0/images/writing-posts-with-ghost.png",
@ -3119,7 +3075,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "How Ghost allows you to turn anonymous readers into an audience of active subscribers, so you know what's working and what isn't.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "How Ghost allows you to turn anonymous readers into an audience of active subscribers, so you know what's working and what isn't.",
"feature_image": "https://static.ghost.org/v4.0.0/images/creating-a-custom-theme.png",
@ -3155,7 +3110,6 @@ Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "For creators and aspiring entrepreneurs looking to generate a sustainable
recurring revenue stream from their creative work, Ghost has built-in payments
@ -3222,7 +3176,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "A guide to collaborating with other staff users to publish, and some resources to help you with the next steps of growing your business",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "A guide to collaborating with other staff users to publish, and some resources to help you with the next steps of growing your business",
"feature_image": "https://static.ghost.org/v4.0.0/images/admin-settings.png",
@ -3258,7 +3211,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "Work with all your favorite apps and tools or create your own custom integrations using the Ghost API.",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "Work with all your favorite apps and tools or create your own custom integrations using the Ghost API.",
"feature_image": "https://static.ghost.org/v4.0.0/images/app-integrations.png",
@ -3295,7 +3247,6 @@ cliffhanger, that's a good time to...",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": " * Lorem
* Aliquam [http://127.0.0.1:2369/about#nowhere]
@ -3345,7 +3296,6 @@ mi vitae est. Mauris placerat eleifend leo.
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "testing
mctesters
@ -3393,7 +3343,6 @@ mctesters
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": null,
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "HTML Ipsum Presents
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac
@ -3436,7 +3385,6 @@ ipsum rutrum or",
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
"custom_excerpt": "This is my custom excerpt!",
"custom_template": null,
"email_recipient_filter": "none",
"email_subject": null,
"excerpt": "This is my custom excerpt!",
"feature_image": "https://example.com/super_photo.jpg",
@ -3471,7 +3419,7 @@ exports[`Posts Content API Can request posts from different origin 2: [headers]
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "46709",
"content-length": "46357",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",

View File

@ -27,8 +27,13 @@ describe('Posts Content API', function () {
before(async function () {
agent = await agentProvider.getContentAPIAgent();
await fixtureManager.init('owner:post', 'users:no-owner', 'user:inactive', 'posts', 'tags:extra', 'api_keys');
await fixtureManager.init('owner:post', 'users:no-owner', 'user:inactive', 'posts', 'tags:extra', 'api_keys', 'newsletters', 'members:newsletters');
agent.authenticate();
// Assign a newsletter to one of the posts
const newsletterId = testUtils.DataGenerator.Content.newsletters[0].id;
const postId = testUtils.DataGenerator.Content.posts[0].id;
await models.Post.edit({newsletter_id: newsletterId}, {id: postId});
});
it('Can request posts', async function () {

View File

@ -23,7 +23,7 @@ const expectedProperties = {
'feature_image_caption',
'featured',
'visibility',
'email_recipient_filter',
'email_segment',
'created_at',
'updated_at',
'published_at',

View File

@ -8,8 +8,8 @@ const config = require('../../../../core/shared/config');
const models = require('../../../../core/server/models');
const localUtils = require('./utils');
const default_newsletter_id = testUtils.DataGenerator.Content.newsletters[0].id;
const second_newsletter_id = testUtils.DataGenerator.Content.newsletters[1].id;
const defaultNewsletterSlug = testUtils.DataGenerator.Content.newsletters[0].slug;
const secondNewsletterSlug = testUtils.DataGenerator.Content.newsletters[1].slug;
describe('Posts API (canary)', function () {
let request;
@ -572,93 +572,6 @@ describe('Posts API (canary)', function () {
});
});
it('publishes a post with email_only and sends email to all without specifying the default newsletter_id', async function () {
const res = await request
.post(localUtils.API.getApiQuery('posts/'))
.set('Origin', config.get('url'))
.send({
posts: [{
title: 'Email me',
email_only: true
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
should.exist(res.body.posts);
should.exist(res.body.posts[0].title);
res.body.posts[0].title.should.equal('Email me');
res.body.posts[0].email_only.should.be.true();
res.body.posts[0].status.should.equal('draft');
should.exist(res.headers.location);
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('posts/')}${res.body.posts[0].id}/`);
const publishedRes = await request
.put(localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/?email_recipient_filter=all`))
.set('Origin', config.get('url'))
.send({
posts: [{
status: 'published',
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.exist(publishedRes.body.posts);
res.body.posts[0].email_only.should.be.true();
publishedRes.body.posts[0].status.should.equal('sent');
should.exist(publishedRes.body.posts[0].email);
publishedRes.body.posts[0].email.email_count.should.equal(4);
});
it('publishes a post while setting email_only flag sends an email to paid without specifying the default newsletter_id', async function () {
const res = await request
.post(localUtils.API.getApiQuery('posts/'))
.set('Origin', config.get('url'))
.send({
posts: [{
title: 'Email me'
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
should.exist(res.body.posts);
should.exist(res.body.posts[0].title);
res.body.posts[0].title.should.equal('Email me');
res.body.posts[0].email_only.should.be.false();
res.body.posts[0].status.should.equal('draft');
should.exist(res.headers.location);
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('posts/')}${res.body.posts[0].id}/`);
const publishedRes = await request
.put(localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/?email_recipient_filter=paid`))
.set('Origin', config.get('url'))
.send({
posts: [{
status: 'published',
email_only: true,
updated_at: res.body.posts[0].updated_at
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.exist(publishedRes.body.posts);
publishedRes.body.posts[0].status.should.equal('sent');
should.exist(publishedRes.body.posts[0].email);
publishedRes.body.posts[0].email.email_count.should.equal(2);
});
it('publishes a post with email_only and sends email to all', async function () {
const res = await request
.post(localUtils.API.getApiQuery('posts/'))
@ -683,7 +596,7 @@ describe('Posts API (canary)', function () {
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('posts/')}${res.body.posts[0].id}/`);
const publishedRes = await request
.put(localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/?email_recipient_filter=all&newsletter_id=${default_newsletter_id}`))
.put(localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/?newsletter=${defaultNewsletterSlug}`))
.set('Origin', config.get('url'))
.send({
posts: [{
@ -726,7 +639,7 @@ describe('Posts API (canary)', function () {
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('posts/')}${res.body.posts[0].id}/`);
const publishedRes = await request
.put(localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/?email_recipient_filter=paid&newsletter_id=${default_newsletter_id}`))
.put(localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/?email_segment=status:-free&newsletter=${defaultNewsletterSlug}`))
.set('Origin', config.get('url'))
.send({
posts: [{
@ -769,7 +682,7 @@ describe('Posts API (canary)', function () {
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('posts/')}${res.body.posts[0].id}/`);
const publishedRes = await request
.put(localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/?email_recipient_filter=paid&newsletter_id=${second_newsletter_id}`))
.put(localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/?email_segment=status:-free&newsletter=${secondNewsletterSlug}`))
.set('Origin', config.get('url'))
.send({
posts: [{
@ -1118,7 +1031,7 @@ describe('Posts API (canary)', function () {
});
});
it('errors with invalid email recipient filter', function () {
it('errors with invalid email segment', function () {
return request
.post(localUtils.API.getApiQuery('posts/'))
.set('Origin', config.get('url'))
@ -1133,7 +1046,7 @@ describe('Posts API (canary)', function () {
.expect(201)
.then((res) => {
return request
.put(`${localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/`)}?email_recipient_filter=not a filter`)
.put(`${localUtils.API.getApiQuery(`posts/${res.body.posts[0].id}/`)}?newsletter=${secondNewsletterSlug}&email_segment=not-a-filter`)
.set('Origin', config.get('url'))
.send({
posts: [{
@ -1148,7 +1061,7 @@ describe('Posts API (canary)', function () {
.expect(400);
})
.then((res) => {
res.text.should.match(/invalid filter/i);
res.text.should.match(/valid filter/i);
});
});
});

View File

@ -37,7 +37,7 @@ const expectedProperties = {
'featured',
'status',
'visibility',
'email_recipient_filter',
'email_segment',
'created_at',
'updated_at',
'published_at',

View File

@ -23,7 +23,6 @@ const expectedProperties = {
'feature_image_caption',
'featured',
'visibility',
'email_recipient_filter',
'created_at',
'updated_at',
'published_at',

View File

@ -60,29 +60,6 @@ describe('Post Model', function () {
});
});
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 () {
@ -717,62 +694,6 @@ describe('Post Model', function () {
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 () {

View File

@ -359,71 +359,5 @@ describe('Unit: canary/utils/serializers/input/posts', function () {
frame.data.posts[0].tags.should.eql([{name: 'name1'}, {name: 'name2'}]);
});
});
describe('transforms legacy email recipient filter values', function () {
it('free becomes status:free', function () {
const frame = {
options: {
email_recipient_filter: 'free'
},
data: {
posts: [{id: '1'}]
}
};
serializers.input.posts.edit({}, frame);
frame.options.email_recipient_filter.should.eql('status:free');
});
it('paid becomes status:-free', function () {
const frame = {
options: {
email_recipient_filter: 'paid'
},
data: {
posts: [{id: '1'}]
}
};
serializers.input.posts.edit({}, frame);
frame.options.email_recipient_filter.should.eql('status:-free');
});
});
});
describe('add', function () {
describe('transforms legacy email recipient filter values', function () {
it('free becomes status:free', function () {
const frame = {
options: {
email_recipient_filter: 'free'
},
data: {
posts: [{id: '1'}]
}
};
serializers.input.posts.add({}, frame);
frame.options.email_recipient_filter.should.eql('status:free');
});
it('paid becomes status:-free', function () {
const frame = {
options: {
email_recipient_filter: 'paid'
},
data: {
posts: [{id: '1'}]
}
};
serializers.input.posts.add({}, frame);
frame.options.email_recipient_filter.should.eql('status:-free');
});
});
});
});

View File

@ -102,5 +102,23 @@ describe('PostsImporter', function () {
should.exist(postWithoutNewsletter);
should.not.exist(postWithoutNewsletter.newsletter_id);
});
it('Maps send_email_when_published', function () {
const fakePosts = [{
slug: 'post-with-newsletter',
send_email_when_published: true
}];
const importer = new PostsImporter({posts: fakePosts});
importer.beforeImport();
const post = find(importer.dataToImport, {slug: 'post-with-newsletter'});
should.exist(post);
post.email_recipient_filter.should.eql('all');
should.not.exist(post.send_email_when_published);
// @TODO: need to check this mapping
//post.newsletter_id.should.eql();
});
});
});

View File

@ -12,30 +12,15 @@ describe('MEGA', function () {
sinon.restore();
});
// via transformEmailRecipientFilter
it('throws when "free" or "paid" strings are used as a email_recipient_filter', async function () {
const postModel = {
get: sinon.stub().returns('free'),
related: sinon.stub().returns({
fetch: sinon.stub().returns(null)
})
};
try {
await addEmail(postModel);
should.fail('addEmail did not throw');
} catch (err) {
should.equal(errors.utils.isGhostError(err), true);
err.message.should.equal('Unexpected email_recipient_filter value "free", expected an NQL equivalent');
}
});
// via transformEmailRecipientFilter
it('throws when "none" is used as a email_recipient_filter', async function () {
const postModel = {
get: sinon.stub().returns('none'),
relations: {},
related: sinon.stub().returns({
fetch: sinon.stub().returns(null)
fetch: sinon.stub().returns({
get: sinon.stub().returns('active')
})
})
};
@ -44,19 +29,45 @@ describe('MEGA', function () {
should.fail('addEmail did not throw');
} catch (err) {
should.equal(errors.utils.isGhostError(err), true);
err.message.should.equal('Cannot send email to "none" email_recipient_filter');
err.message.should.equal('Cannot send email to "none" email_segment');
}
});
it('throws when sending to an archived newsletter', async function () {
const postModel = {
get: sinon.stub().returns('all'),
relations: {},
related: sinon.stub().returns({
fetch: sinon.stub().returns({
get: sinon.stub().returns('archived')
})
})
};
try {
await addEmail(postModel);
should.fail('addEmail did not throw');
} catch (err) {
should.equal(errors.utils.isGhostError(err), true);
err.message.should.equal('Cannot send email to archived newsletters');
}
});
// via transformEmailRecipientFilter
it('throws when "public" is used as newsletter.visibility', async function () {
const newsletterGetter = sinon.stub();
newsletterGetter.withArgs('status').returns('active');
newsletterGetter.withArgs('visibility').returns('public');
const postModel = {
get: sinon.stub().returns('status:free'),
fetch: sinon.stub().returns(Promise.resolve({
get: () => 'public'
}))
relations: {},
related: sinon.stub().returns({
fetch: sinon.stub().returns({
get: newsletterGetter
})
})
};
postModel.related = sinon.stub().returns(postModel);
sinon.stub(labs, 'isSet').returns(true);
try {
@ -70,18 +81,21 @@ describe('MEGA', function () {
});
describe('transformEmailRecipientFilter', function () {
it('enforces subscribed:true with correct operator precedence', function () {
const transformedFilter = _transformEmailRecipientFilter('status:free,status:-free');
transformedFilter.should.equal('subscribed:true+(status:free,status:-free)');
});
it('public newsletter', function () {
const newsletterGetter = sinon.stub();
newsletterGetter.withArgs('status').returns('active');
newsletterGetter.withArgs('visibility').returns('members');
it('doesn\'t enforce subscribed:true when sending an email to a newsletter', function () {
const transformedFilter = _transformEmailRecipientFilter('status:free,status:-free', {}, {id: 'test', get: () => 'members'});
const transformedFilter = _transformEmailRecipientFilter({id: 'test', get: newsletterGetter}, 'status:free,status:-free', 'field');
transformedFilter.should.equal('newsletters.id:test+(status:free,status:-free)');
});
it('combines successfully with the newsletter paid-only visibility', function () {
const transformedFilter = _transformEmailRecipientFilter('status:free,status:-free', {}, {id: 'test', get: () => 'paid'});
it('paid-only newsletter', function () {
const newsletterGetter = sinon.stub();
newsletterGetter.withArgs('status').returns('active');
newsletterGetter.withArgs('visibility').returns('paid');
const transformedFilter = _transformEmailRecipientFilter({id: 'test', get: newsletterGetter}, 'status:free,status:-free', 'field');
transformedFilter.should.equal('newsletters.id:test+(status:free,status:-free)+status:-free');
});
});
@ -125,31 +139,18 @@ describe('MEGA', function () {
});
describe('getEmailMemberRows', function () {
it('addEmail throws when "free" or "paid" strings are used as a recipient_filter', async function () {
const emailModel = {
get: sinon.stub().returns('paid'),
related: sinon.stub().returns({
fetch: sinon.stub().returns({
id: 'test'
})
})
};
it('getEmailMemberRows throws when "none" is used as a recipient_filter', async function () {
const newsletterGetter = sinon.stub();
newsletterGetter.withArgs('status').returns('active');
newsletterGetter.withArgs('visibility').returns('members');
try {
await _getEmailMemberRows({emailModel});
should.fail('getEmailMemberRows did not throw');
} catch (err) {
should.equal(errors.utils.isGhostError(err), true);
err.message.should.equal('Unexpected recipient_filter value "paid", expected an NQL equivalent');
}
});
it('addEmail throws when "none" is used as a recipient_filter', async function () {
const emailModel = {
get: sinon.stub().returns('none'),
relations: {},
related: sinon.stub().returns({
fetch: sinon.stub().returns({
id: 'test'
id: 'test',
newsletterGetter
})
})
};

View File

@ -372,7 +372,8 @@ DataGenerator.Content = {
show_header_icon: true,
show_header_title: true,
show_badge: true,
sort_order: 1
sort_order: 1,
header_image: '__GHOST_URL__/content/images/2022/05/test.jpg'
},
{
id: ObjectId().toHexString(),
@ -389,7 +390,8 @@ DataGenerator.Content = {
show_header_icon: true,
show_header_title: true,
show_badge: true,
sort_order: 2
sort_order: 2,
header_image: '__GHOST_URL__/content/images/2022/05/test.jpg'
},
{
id: ObjectId().toHexString(),
@ -406,7 +408,8 @@ DataGenerator.Content = {
show_header_icon: true,
show_header_title: true,
show_badge: true,
sort_order: 2
sort_order: 2,
header_image: '__GHOST_URL__/content/images/2022/05/test.jpg'
}
],