0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Fixed newsletter includes when adding or editing (#14696)

refs https://github.com/TryGhost/Team/issues/1571
refs https://ghost.slack.com/archives/C02G9E68C/p1650986988322609

- Makes sure the includes are always included
- Moved read to the newsletter service
- Added tests
- Updated unit tests to work with multiple findOne calls
- Fixed reject assertions not correctly awaiting in unit tests
This commit is contained in:
Simon Backx 2022-05-05 11:20:15 +02:00 committed by GitHub
parent 366a7be36d
commit 38b9cf2472
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 282 additions and 76 deletions

View file

@ -1,11 +1,6 @@
const models = require('../../models');
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const allowedIncludes = ['count.posts', 'count.members'];
const messages = {
newsletterNotFound: 'Newsletter not found.'
};
const newslettersService = require('../../services/newsletters');
module.exports = {
@ -56,14 +51,7 @@ module.exports = {
},
permissions: true,
async query(frame) {
const newsletter = models.Newsletter.findOne(frame.data, frame.options);
if (!newsletter) {
throw new errors.NotFoundError({
message: tpl(messages.newsletterNotFound)
});
}
return newsletter;
return newslettersService.read(frame.data, frame.options);
}
},

View file

@ -7,7 +7,8 @@ const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const messages = {
nameAlreadyExists: 'A newsletter with the same name already exists'
nameAlreadyExists: 'A newsletter with the same name already exists',
newsletterNotFound: 'Newsletter not found.'
};
class NewslettersService {
@ -79,6 +80,23 @@ class NewslettersService {
});
}
/**
* @public
* @param {Object} options data (id, uuid, slug...)
* @param {Object} [options] options
* @returns {Promise<object>} JSONified Newsletter models
*/
async read(data, options = {}) {
const newsletter = await this.NewsletterModel.findOne(data, options);
if (!newsletter) {
throw new errors.NotFoundError({
message: tpl(messages.newsletterNotFound)
});
}
return newsletter;
}
/**
* @public
* @param {Object} [options] options
@ -89,6 +107,7 @@ class NewslettersService {
return newsletters.toJSON();
}
/**
* @public
* @param {object} attrs model properties
@ -129,6 +148,9 @@ class NewslettersService {
throw error;
}
// Load relations correctly
newsletter = await this.NewsletterModel.findOne({id: newsletter.id}, {...options, require: true});
// subscribe existing members if opt_in_existing=true
if (options.opt_in_existing) {
debug(`Subscribing members to newsletter '${newsletter.get('name')}'`);
@ -153,12 +175,13 @@ class NewslettersService {
/**
* @public
* @param {object} attrs model properties
* @param {Object} [options] options
* @param {Object} options options
* @param {string} options.id Newsletter id to edit
* @returns {Promise<{object}>} Newsetter Model with verification metadata
*/
async edit(attrs, options = {}) {
async edit(attrs, options) {
// fetch newsletter first so we can compare changed emails
const originalNewsletter = await this.NewsletterModel.findOne(options, {require: true});
const originalNewsletter = await this.NewsletterModel.findOne({id: options.id}, {require: true});
const {cleanedAttrs, emailsToVerify} = await this.prepAttrsForEmailVerification(attrs, originalNewsletter);
@ -176,8 +199,12 @@ class NewslettersService {
throw error;
}
// Load relations correctly in the response
updatedNewsletter = await this.NewsletterModel.findOne({id: updatedNewsletter.id}, {...options, require: true});
return this.respondWithEmailVerification(updatedNewsletter, emailsToVerify);
await this.respondWithEmailVerification(updatedNewsletter, emailsToVerify);
return updatedNewsletter;
}
/**

View file

@ -23,7 +23,7 @@ Object {
"show_header_name": true,
"show_header_title": true,
"slug": "new-newsletter-with-existing-members-subscribed",
"sort_order": 7,
"sort_order": 8,
"status": "active",
"subscribe_on_signup": true,
"title_alignment": "center",
@ -75,7 +75,7 @@ Object {
"show_header_name": true,
"show_header_title": true,
"slug": "my-test-newsletter-with-custom-sender_email",
"sort_order": 6,
"sort_order": 7,
"status": "active",
"subscribe_on_signup": true,
"title_alignment": "center",
@ -128,7 +128,7 @@ Object {
"show_header_name": true,
"show_header_title": true,
"slug": "my-test-newsletter-with-custom-sender_email-and-subscribe-existing",
"sort_order": 8,
"sort_order": 10,
"status": "active",
"subscribe_on_signup": true,
"title_alignment": "center",
@ -145,7 +145,7 @@ exports[`Newsletters API Can add a newsletter - with custom sender_email and sub
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": "826",
"content-length": "827",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/newsletters\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
@ -274,7 +274,7 @@ Object {
"show_header_name": true,
"show_header_title": true,
"slug": "my-first-test-newsletter",
"sort_order": 4,
"sort_order": 5,
"status": "active",
"subscribe_on_signup": true,
"title_alignment": "center",
@ -321,7 +321,7 @@ Object {
"show_header_name": true,
"show_header_title": true,
"slug": "my-second-test-newsletter",
"sort_order": 5,
"sort_order": 6,
"status": "active",
"subscribe_on_signup": true,
"title_alignment": "center",
@ -578,6 +578,57 @@ Object {
}
`;
exports[`Newsletters API Can include members & posts counts when adding a newsletter 1: [body] 1`] = `
Object {
"newsletters": Array [
Object {
"body_font_category": "serif",
"count": Object {
"members": 0,
"posts": 0,
},
"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,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "My test newsletter 2",
"sender_email": null,
"sender_name": "Test",
"sender_reply_to": "newsletter",
"show_badge": true,
"show_feature_image": true,
"show_header_icon": true,
"show_header_name": true,
"show_header_title": true,
"slug": "my-test-newsletter-2",
"sort_order": 4,
"status": "active",
"subscribe_on_signup": true,
"title_alignment": "center",
"title_font_category": "serif",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "members",
},
],
}
`;
exports[`Newsletters API Can include members & posts counts when adding 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": "688",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/newsletters\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
"vary": "Origin, Accept-Encoding",
"x-cache-invalidate": "/*",
"x-powered-by": "Express",
}
`;
exports[`Newsletters API Can include members & posts counts when browsing newsletters 1: [body] 1`] = `
Object {
"meta": Object {
@ -727,6 +778,56 @@ Object {
}
`;
exports[`Newsletters API Can include members & posts counts when editing newsletters 1: [body] 1`] = `
Object {
"newsletters": Array [
Object {
"body_font_category": "serif",
"count": Object {
"members": 4,
"posts": 0,
},
"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,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Updated newsletter name 2",
"sender_email": "jamie@example.com",
"sender_name": "Jamie",
"sender_reply_to": "newsletter",
"show_badge": true,
"show_feature_image": true,
"show_header_icon": true,
"show_header_name": true,
"show_header_title": true,
"slug": "daily-newsletter",
"sort_order": 1,
"status": "active",
"subscribe_on_signup": false,
"title_alignment": "center",
"title_font_category": "serif",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "members",
},
],
}
`;
exports[`Newsletters API Can include members & posts counts when editing 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": "706",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
"x-cache-invalidate": "/*",
"x-powered-by": "Express",
}
`;
exports[`Newsletters API Can include members & posts counts when reading a newsletter 1: [body] 1`] = `
Object {
"newsletters": Array [

View file

@ -146,6 +146,36 @@ describe('Newsletters API', function () {
header_image.should.equal(transformReadyPath);
});
it('Can include members & posts counts when adding a newsletter', async function () {
const newsletter = {
uuid: uuid.v4(),
name: 'My test newsletter 2',
sender_name: 'Test',
sender_email: null,
sender_reply_to: 'newsletter',
status: 'active',
subscribe_on_signup: true,
title_font_category: 'serif',
body_font_category: 'serif',
show_header_icon: true,
show_header_title: true,
show_badge: true,
sort_order: 0
};
await agent
.post(`newsletters/?include=count.members,count.posts`)
.body({newsletters: [newsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
});
it('Can add multiple newsletters', async function () {
const firstNewsletter = {
name: 'My first test newsletter'
@ -268,6 +298,23 @@ describe('Newsletters API', function () {
});
});
it('Can include members & posts counts when editing newsletters', async function () {
const id = fixtureManager.get('newsletters', 0).id;
await agent.put(`newsletters/${id}/?include=count.members,count.posts`)
.body({
newsletters: [{
name: 'Updated newsletter name 2'
}]
})
.expectStatus(200)
.matchBodySnapshot({
newsletters: [newsletterSnapshot]
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it('Can edit a newsletters and update the sender_email when already set', async function () {
const id = fixtureManager.get('newsletters', 0).id;
@ -359,8 +406,44 @@ describe('Newsletters API', function () {
});
});
it(`Can't add multiple newsletters with same name`, async function () {
const firstNewsletter = {
name: 'Duplicate newsletter'
};
const secondNewsletter = {...firstNewsletter};
await agent
.post(`newsletters/`)
.body({newsletters: [firstNewsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshotWithoutSortOrder]
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
await agent
.post(`newsletters/`)
.body({newsletters: [secondNewsletter]})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyUuid,
message: 'Validation error, cannot save newsletter.',
context: 'A newsletter with the same name already exists'
}]
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it('Can add a newsletter - with custom sender_email and subscribe existing members', async function () {
if (DatabaseInfo.isSQLite(db.knex)) {
// This breaks snapshot tests if you don't update snapshot tests on MySQL + make sure this is the last ADD test
return;
}
const newsletter = {
@ -399,41 +482,6 @@ describe('Newsletters API', function () {
});
});
it(`Can't add multiple newsletters with same name`, async function () {
const firstNewsletter = {
name: 'Duplicate newsletter'
};
const secondNewsletter = {...firstNewsletter};
await agent
.post(`newsletters/`)
.body({newsletters: [firstNewsletter]})
.expectStatus(201)
.matchBodySnapshot({
newsletters: [newsletterSnapshotWithoutSortOrder]
})
.matchHeaderSnapshot({
etag: anyEtag,
location: anyLocationFor('newsletters')
});
await agent
.post(`newsletters/`)
.body({newsletters: [secondNewsletter]})
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyUuid,
message: 'Validation error, cannot save newsletter.',
context: 'A newsletter with the same name already exists'
}]
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it(`Can't edit multiple newsletters to existing name`, async function () {
const id = fixtureManager.get('newsletters', 0).id;

View file

@ -47,6 +47,7 @@ describe('NewslettersService', function () {
beforeEach(function () {
getStub = sinon.stub();
getStub.withArgs('id').returns('test');
sinon.spy(tokenProvider, 'create');
sinon.spy(tokenProvider, 'validate');
mockManager.mockMail();
@ -56,6 +57,26 @@ describe('NewslettersService', function () {
mockManager.restore();
});
describe('read', function () {
let findOneStub;
beforeEach(function () {
// Stub edit as a function that returns its first argument
findOneStub = sinon.stub(models.Newsletter, 'findOne').returns({get: getStub, id: 'test'});
});
it('returns the result of findOne', async function () {
const result = await newsletterService.read({slug: 'my-slug'});
sinon.assert.calledOnceWithExactly(findOneStub, {slug: 'my-slug'}, {});
assert.equal(result.id, 'test');
});
it('throws a not found error if not found', async function () {
findOneStub = findOneStub.returns(null);
await assert.rejects(newsletterService.read({slug: 'my-slug'}), {errorType: 'NotFoundError'});
sinon.assert.calledOnceWithExactly(findOneStub, {slug: 'my-slug'}, {});
});
});
// @TODO replace this with a specific function for fetching all available newsletters
describe('browse', function () {
it('lists all newsletters by calling findAll and toJSON', async function () {
@ -70,15 +91,16 @@ describe('NewslettersService', function () {
});
describe('add', function () {
let addStub, fetchMembersStub, fakeMemberIds, subscribeStub, getNextAvailableSortOrderStub;
let addStub, fetchMembersStub, fakeMemberIds, subscribeStub, getNextAvailableSortOrderStub, findOneStub;
beforeEach(function () {
fakeMemberIds = new Array(3).fill({id: 1});
subscribeStub = sinon.stub().returns(fakeMemberIds);
// Stub add as a function that returns get & subscribeMembersById methods
addStub = sinon.stub(models.Newsletter, 'add').returns({get: getStub, subscribeMembersById: subscribeStub});
addStub = sinon.stub(models.Newsletter, 'add').returns({get: getStub, id: 'test', subscribeMembersById: subscribeStub});
fetchMembersStub = sinon.stub(models.Member, 'fetchAllSubscribed').returns([]);
getNextAvailableSortOrderStub = sinon.stub(models.Newsletter, 'getNextAvailableSortOrder').returns(1);
findOneStub = sinon.stub(models.Newsletter, 'findOne').returns({get: getStub, id: 'test', subscribeMembersById: subscribeStub});
});
it('rejects if the limit services determines it would be over the limit', async function () {
@ -98,10 +120,11 @@ describe('NewslettersService', function () {
});
it('rejects if called with no data', async function () {
assert.rejects(await newsletterService.add, {name: 'TypeError'});
await assert.rejects(newsletterService.add(), {name: 'TypeError'});
sinon.assert.notCalled(addStub);
sinon.assert.notCalled(getNextAvailableSortOrderStub);
sinon.assert.notCalled(fetchMembersStub);
sinon.assert.notCalled(findOneStub);
});
it('will attempt to add empty object without verification', async function () {
@ -111,6 +134,7 @@ describe('NewslettersService', function () {
sinon.assert.calledOnce(getNextAvailableSortOrderStub);
sinon.assert.notCalled(fetchMembersStub);
sinon.assert.calledOnceWithExactly(addStub, {sort_order: 1}, {});
sinon.assert.calledOnceWithExactly(findOneStub, {id: 'test'}, {require: true});
});
it('will override sort_order', async function () {
@ -122,6 +146,7 @@ describe('NewslettersService', function () {
sinon.assert.calledOnce(getNextAvailableSortOrderStub);
sinon.assert.notCalled(fetchMembersStub);
sinon.assert.calledOnceWithExactly(addStub, {name: 'hello world', sort_order: 1}, options);
sinon.assert.calledOnceWithExactly(findOneStub, {id: 'test'}, {foo: 'bar', require: true});
});
it('will pass object and options through to model when there are no fields needing verification', async function () {
@ -133,6 +158,7 @@ describe('NewslettersService', function () {
assert.equal(result.meta, undefined); // meta property has not been added
sinon.assert.calledOnceWithExactly(addStub, data, options);
sinon.assert.notCalled(fetchMembersStub);
sinon.assert.calledOnceWithExactly(findOneStub, {id: 'test'}, {foo: 'bar', require: true});
});
it('will trigger verification when sender_email is provided', async function () {
@ -148,8 +174,9 @@ describe('NewslettersService', function () {
});
sinon.assert.calledOnceWithExactly(addStub, {name: 'hello world', sort_order: 1}, options);
mockManager.assert.sentEmail({to: 'test@example.com'});
sinon.assert.calledOnceWithExactly(tokenProvider.create, {id: undefined, property: 'sender_email', value: 'test@example.com'});
sinon.assert.calledOnceWithExactly(tokenProvider.create, {id: 'test', property: 'sender_email', value: 'test@example.com'});
sinon.assert.notCalled(fetchMembersStub);
sinon.assert.calledOnceWithExactly(findOneStub, {id: 'test'}, {foo: 'bar', require: true});
});
it('will try to find existing members when opt_in_existing is provided', async function () {
@ -165,6 +192,7 @@ describe('NewslettersService', function () {
sinon.assert.calledOnceWithExactly(addStub, {name: 'hello world', sort_order: 1}, options);
mockManager.assert.sentEmailCount(0);
sinon.assert.calledOnce(fetchMembersStub);
sinon.assert.calledOnceWithExactly(findOneStub, {id: 'test'}, {opt_in_existing: true, transacting: options.transacting, require: true});
});
it('will try to subscribe existing members when opt_in_existing provided + members exist', async function () {
@ -190,36 +218,42 @@ describe('NewslettersService', function () {
let editStub, findOneStub;
beforeEach(function () {
// Stub edit as a function that returns its first argument
editStub = sinon.stub(models.Newsletter, 'edit').returns({get: getStub});
findOneStub = sinon.stub(models.Newsletter, 'findOne').returns({get: getStub});
editStub = sinon.stub(models.Newsletter, 'edit').returns({get: getStub, id: 'test'});
findOneStub = sinon.stub(models.Newsletter, 'findOne').returns({get: getStub, id: 'test'});
});
it('rejects if called with no data', async function () {
assert.rejects(await newsletterService.add, {name: 'TypeError'});
await assert.rejects(newsletterService.add(), {name: 'TypeError'});
sinon.assert.notCalled(editStub);
});
it('will attempt to add empty object without verification', async function () {
const result = await newsletterService.edit({});
const result = await newsletterService.edit({}, {id: 'test'});
assert.equal(result.meta, undefined); // meta property has not been added
sinon.assert.calledOnceWithExactly(editStub, {}, {});
sinon.assert.calledOnceWithExactly(editStub, {}, {id: 'test'});
});
it('will pass object and options through to model when there are no fields needing verification', async function () {
const data = {name: 'hello world'};
const options = {foo: 'bar'};
const options = {foo: 'bar', id: 'test'};
const result = await newsletterService.edit(data, options);
assert.equal(result.meta, undefined); // meta property has not been added
sinon.assert.calledOnceWithExactly(editStub, data, options);
sinon.assert.calledOnceWithExactly(findOneStub, options, {require: true});
sinon.assert.calledTwice(findOneStub);
sinon.assert.calledWithExactly(findOneStub.firstCall, {id: 'test'}, {require: true});
sinon.assert.calledWithExactly(findOneStub.secondCall, {id: 'test'}, {...options, require: true});
});
it('will trigger verification when sender_email is provided', async function () {
const data = {name: 'hello world', sender_email: 'test@example.com'};
const options = {foo: 'bar'};
const options = {id: 'test', foo: 'bar'};
// Explicitly set the old value to a different value
getStub.withArgs('sender_email').returns('old@example.com');
const result = await newsletterService.edit(data, options);
@ -229,14 +263,18 @@ describe('NewslettersService', function () {
]
});
sinon.assert.calledOnceWithExactly(editStub, {name: 'hello world'}, options);
sinon.assert.calledOnceWithExactly(findOneStub, options, {require: true});
sinon.assert.calledTwice(findOneStub);
sinon.assert.calledWithExactly(findOneStub.firstCall, {id: 'test'}, {require: true});
sinon.assert.calledWithExactly(findOneStub.secondCall, {id: 'test'}, {...options, require: true});
mockManager.assert.sentEmail({to: 'test@example.com'});
sinon.assert.calledOnceWithExactly(tokenProvider.create, {id: undefined, property: 'sender_email', value: 'test@example.com'});
sinon.assert.calledOnceWithExactly(tokenProvider.create, {id: 'test', property: 'sender_email', value: 'test@example.com'});
});
it('will NOT trigger verification when sender_email is provided but is already verified', async function () {
const data = {name: 'hello world', sender_email: 'test@example.com'};
const options = {foo: 'bar'};
const options = {foo: 'bar', id: 'test'};
// The model says this is already verified
getStub.withArgs('sender_email').returns('test@example.com');
@ -245,7 +283,11 @@ describe('NewslettersService', function () {
assert.deepEqual(result.meta, undefined);
sinon.assert.calledOnceWithExactly(editStub, {name: 'hello world', sender_email: 'test@example.com'}, options);
sinon.assert.calledOnceWithExactly(findOneStub, options, {require: true});
sinon.assert.calledTwice(findOneStub);
sinon.assert.calledWithExactly(findOneStub.firstCall, {id: 'test'}, {require: true});
sinon.assert.calledWithExactly(findOneStub.secondCall, {id: 'test'}, {...options, require: true});
mockManager.assert.sentEmailCount(0);
});
});
@ -259,7 +301,7 @@ describe('NewslettersService', function () {
});
it('rejects if called with no data', async function () {
assert.rejects(await newsletterService.verifyPropertyUpdate, {name: 'TypeError'});
await assert.rejects(newsletterService.verifyPropertyUpdate(), {name: 'SyntaxError'});
});
it('Updates model with values from token', async function () {