Ghost/ghost/core/test/e2e-server/services/mentions.test.js
Simon Backx 098f4353a7
Disabled network retries for webmentions in tests (#18269)
refs https://ghost.slack.com/archives/C02G9E68C/p1695296293667689

- We block all outgoing networking by default in tests. When editing a
post/page, Ghost tries to send a webmention. Because of my earlier
changes
(1e3232cf82)
it tries 3 times - all network requests fail instantly when networking
is disabled - but it adds a delay in between those retries. So my change
disables retries during tests.
- Adds a warning when afterEach hook tooks longer than 2s due to
awaiting jobs/events
- We reduce the amount of webmentions that are send, by only sending
webmentions for added or removed urls, no longer when a post html is
changed (unless post is published/removed).
2023-09-21 16:17:05 +02:00

517 lines
20 KiB
JavaScript

const {agentProvider, fixtureManager, mockManager} = require('../../utils/e2e-framework');
const nock = require('nock');
const assert = require('assert/strict');
const markdownToMobiledoc = require('../../utils/fixtures/data-generator').markdownToMobiledoc;
const jobsService = require('../../../core/server/services/mentions-jobs');
let agent;
let mentionUrl = new URL('https://www.otherghostsite.com/');
let mentionUrl2 = new URL('https://www.otherghostsite2.com/');
let mentionHtml = `Check out this really cool <a href="${mentionUrl.href}">other site</a>.`;
let mentionHtml2 = `Check out this really cool <a href="${mentionUrl2.href}">other site</a>.`;
let endpointUrl = new URL('https://www.endpoint.com/');
let endpointUrl2 = new URL('https://www.endpoint2.com/');
let targetHtml = `<head><link rel="webmention" href="${endpointUrl.href}"</head><body>Some content</body>`;
let targetHtml2 = `<head><link rel="webmention" href="${endpointUrl2.href}"</head><body>Some content</body>`;
let mentionMock;
let endpointMock;
const DomainEvents = require('@tryghost/domain-events');
const mentionsPost = {
title: 'testing sending webmentions',
mobiledoc: markdownToMobiledoc(mentionHtml)
};
const editedMentionsPost = {
title: 'testing sending webmentions',
mobiledoc: markdownToMobiledoc(mentionHtml2)
};
function addMentionMocks() {
// mock response from website mentioned by post to provide endpoint
mentionMock = nock(mentionUrl.href)
.persist()
.get('/')
.reply(200, targetHtml, {'content-type': 'text/html'});
// mock response from mention endpoint, usually 201, sometimes 202
endpointMock = nock(endpointUrl.href)
.persist()
.post('/')
.reply(201);
}
describe('Mentions Service', function () {
before(async function () {
agent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('users');
await agent.loginAsAdmin();
});
beforeEach(async function () {
// externalRequest does dns lookup; stub to make sure we don't fail with fake domain names
mockManager.disableNetwork();
// mock response from website mentioned by post to provide endpoint
addMentionMocks();
await jobsService.allSettled();
await DomainEvents.allSettled();
});
afterEach(async function () {
mockManager.restore();
});
describe('Sending Service', function () {
describe(`does not send when we expect it to not send`, function () {
it('New draft post created', async function () {
const draftPost = {status: 'draft', ...mentionsPost};
await agent
.post('posts/')
.body({posts: [draftPost]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMock.isDone(), false);
assert.equal(endpointMock.isDone(), false);
});
it('Email only post published', async function () {
const publishedPost = {status: 'published', email_only: true, ...mentionsPost};
await agent
.post('posts/')
.body({posts: [publishedPost]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMock.isDone(), false);
assert.equal(endpointMock.isDone(), false);
});
it('Post without content', async function () {
const publishedPost = {status: 'published', mobiledoc: markdownToMobiledoc(''), title: 'empty post'};
await agent
.post('posts/')
.body({posts: [publishedPost]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMock.isDone(), false);
assert.equal(endpointMock.isDone(), false);
});
it('New draft page created', async function () {
const draftPage = {status: 'draft', ...mentionsPost};
await agent
.post('pages/')
.body({pages: [draftPage]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMock.isDone(), false);
assert.equal(endpointMock.isDone(), false);
});
});
describe(`does send when we expect it to send`, function () {
it('Newly published post (post.published)', async function () {
let publishedPost = {status: 'published', ...mentionsPost};
await agent
.post('posts/')
.body({posts: [publishedPost]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
});
it('Does not send for edited post without url changes (post.published.edited)', async function () {
const publishedPost = {status: 'published', ...mentionsPost};
const res = await agent
.post('posts/')
.body({posts: [publishedPost]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
// while not the point of the test, we should have real links/mentions to start with
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
nock.cleanAll();
addMentionMocks();
assert.equal(mentionMock.isDone(), false, 'should be reset');
assert.equal(endpointMock.isDone(), false, 'should be reset');
const postId = res.body.posts[0].id;
const editedPost = {
mobiledoc: markdownToMobiledoc(mentionHtml + 'More content'),
updated_at: res.body.posts[0].updated_at
};
await agent.put(`posts/${postId}/`)
.body({posts: [editedPost]})
.expectStatus(200);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMock.isDone(), false);
assert.equal(endpointMock.isDone(), false);
});
it('Does send for edited post with url changes (post.published.edited)', async function () {
const publishedPost = {status: 'published', ...mentionsPost};
const res = await agent
.post('posts/')
.body({posts: [publishedPost]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
// while not the point of the test, we should have real links/mentions to start with
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
nock.cleanAll();
addMentionMocks();
assert.equal(mentionMock.isDone(), false, 'should be reset');
assert.equal(endpointMock.isDone(), false, 'should be reset');
// reset mocks for mention
const mentionMockTwo = nock(mentionUrl2.href)
.persist()
.get('/')
.reply(200, targetHtml2, {'content-type': 'text/html'});
const endpointMockTwo = nock(endpointUrl2.href)
.persist()
.post('/')
.reply(201);
const postId = res.body.posts[0].id;
const editedPost = {
...editedMentionsPost,
updated_at: res.body.posts[0].updated_at
};
await agent.put(`posts/${postId}/`)
.body({posts: [editedPost]})
.expectStatus(200);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMockTwo.isDone(), true);
assert.equal(endpointMockTwo.isDone(), true);
// Also send again to the deleted url
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
});
it('Unpublished post (post.unpublished)', async function () {
const publishedPost = {status: 'published', ...mentionsPost};
const res = await agent
.post('posts/')
.body({posts: [publishedPost]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
// while not the point of the test, we should have real links/mentions to start with
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
nock.cleanAll();
// reset mocks for mention
const mentionMockTwo = nock(mentionUrl.href)
.persist()
.get('/')
.reply(200, targetHtml, {'content-type': 'text/html'});
const endpointMockTwo = nock(endpointUrl.href)
.persist()
.post('/')
.reply(201);
const postId = res.body.posts[0].id;
// moving back to draft is how we unpublish
const unpublishedPost = {
status: 'draft',
updated_at: res.body.posts[0].updated_at
};
await agent.put(`posts/${postId}/`)
.body({posts: [unpublishedPost]})
.expectStatus(200);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMockTwo.isDone(), true);
assert.equal(endpointMockTwo.isDone(), true);
});
it('Newly published page (page.published)', async function () {
let publishedPage = {status: 'published', ...mentionsPost};
await agent
.post('pages/')
.body({pages: [publishedPage]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
});
it('Edited published page without url changes (page.published.edited)', async function () {
const publishedPage = {status: 'published', ...mentionsPost};
const res = await agent
.post('pages/')
.body({pages: [publishedPage]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
// while not the point of the test, we should have real links/mentions to start with
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
nock.cleanAll();
addMentionMocks();
assert.equal(mentionMock.isDone(), false, 'should be reset');
assert.equal(endpointMock.isDone(), false, 'should be reset');
const pageId = res.body.pages[0].id;
const editedPage = {
mobiledoc: markdownToMobiledoc(mentionHtml + 'More content'),
updated_at: res.body.pages[0].updated_at
};
await agent.put(`pages/${pageId}/`)
.body({pages: [editedPage]})
.expectStatus(200);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMock.isDone(), false);
assert.equal(mentionMock.isDone(), false);
});
it('Edited published page with url changes (page.published.edited)', async function () {
const publishedPage = {status: 'published', ...mentionsPost};
const res = await agent
.post('pages/')
.body({pages: [publishedPage]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
// while not the point of the test, we should have real links/mentions to start with
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
nock.cleanAll();
addMentionMocks();
assert.equal(mentionMock.isDone(), false, 'should be reset');
assert.equal(endpointMock.isDone(), false, 'should be reset');
// reset mocks for mention
const mentionMockTwo = nock(mentionUrl2.href)
.persist()
.get('/')
.reply(200, targetHtml2, {'content-type': 'text/html'});
const endpointMockTwo = nock(endpointUrl2.href)
.persist()
.post('/')
.reply(201);
const pageId = res.body.pages[0].id;
const editedPage = {
...editedMentionsPost,
updated_at: res.body.pages[0].updated_at
};
await agent.put(`pages/${pageId}/`)
.body({pages: [editedPage]})
.expectStatus(200);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMockTwo.isDone(), true);
assert.equal(endpointMockTwo.isDone(), true);
// Also send again to the deleted url
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
});
it('Unpublished post (post.unpublished)', async function () {
const publishedPage = {status: 'published', ...mentionsPost};
const res = await agent
.post('pages/')
.body({pages: [publishedPage]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
// while not the point of the test, we should have real links/mentions to start with
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
nock.cleanAll();
// reset mocks for mention
const mentionMockTwo = nock(mentionUrl.href)
.persist()
.get('/')
.reply(200, targetHtml, {'content-type': 'text/html'});
const endpointMockTwo = nock(endpointUrl.href)
.persist()
.post('/')
.reply(201);
const pageId = res.body.pages[0].id;
// moving back to draft is how we unpublish
const unpublishedPage = {
status: 'draft',
updated_at: res.body.pages[0].updated_at
};
await agent.put(`pages/${pageId}/`)
.body({pages: [unpublishedPage]})
.expectStatus(200);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMockTwo.isDone(), true);
assert.equal(endpointMockTwo.isDone(), true);
});
it('Sends for links that got removed from a post', async function () {
const publishedPost = {status: 'published', ...mentionsPost};
const res = await agent
.post('posts/')
.body({posts: [publishedPost]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
// while not the point of the test, we should have real links/mentions to start with
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
nock.cleanAll();
// reset mocks for mention
const mentionMockTwo = nock(mentionUrl.href)
.persist()
.get('/')
.reply(200, targetHtml, {'content-type': 'text/html'});
const endpointMockTwo = nock(endpointUrl.href)
.persist()
.post('/')
.reply(201);
const postId = res.body.posts[0].id;
const editedPost = {
mobiledoc: markdownToMobiledoc(`mentions were removed from this post`),
updated_at: res.body.posts[0].updated_at
};
await agent.put(`posts/${postId}/`)
.body({posts: [editedPost]})
.expectStatus(200);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMockTwo.isDone(), true);
assert.equal(endpointMockTwo.isDone(), true);
});
it('Sends for links that got removed from a page', async function () {
const publishedPage = {status: 'published', ...mentionsPost};
const res = await agent
.post('pages/')
.body({pages: [publishedPage]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
// while not the point of the test, we should have real links/mentions to start with
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
nock.cleanAll();
// reset mocks for mention
const mentionMockTwo = nock(mentionUrl.href)
.persist()
.get('/')
.reply(200, targetHtml, {'content-type': 'text/html'});
const endpointMockTwo = nock(endpointUrl.href)
.persist()
.post('/')
.reply(201);
const pageId = res.body.pages[0].id;
const editedPage = {
mobiledoc: markdownToMobiledoc(`mentions were removed from this post`),
updated_at: res.body.pages[0].updated_at
};
await agent.put(`pages/${pageId}/`)
.body({pages: [editedPage]})
.expectStatus(200);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMockTwo.isDone(), true);
assert.equal(endpointMockTwo.isDone(), true);
});
// there's no special handling for this atm, but could be down the road
it('New paid post', async function () {
const publishedPost = {status: 'published', visibility: 'paid', ...mentionsPost};
await agent
.post('posts/')
.body({posts: [publishedPost]})
.expectStatus(201);
await jobsService.allSettled();
await DomainEvents.allSettled();
assert.equal(mentionMock.isDone(), true);
assert.equal(endpointMock.isDone(), true);
});
});
});
});