0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added basic framework for webhook e2e tests

refs https://github.com/TryGhost/Toolbox/issues/320

- This is an **MVP** to be able to intercept and match webhook request
snapshots. The concept is similar to the one used in API E2E tests using
same "matchBodySnapshot" and other "match*" methods to test the webhook
**request** data
- Next up here would be:
1. Header matcher
2. Mocking more than one webhook (and doing something nicer with the way
the fixture data is inserted, does this logic belong to the mock-receiver?
This commit is contained in:
Naz 2022-05-26 11:11:41 +08:00 committed by naz
parent 6a3f61f62a
commit 0f4aeaaa80
4 changed files with 268 additions and 0 deletions

View file

@ -0,0 +1,113 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`post.* events post.published even is triggered 1: [body] 1`] = `
Object {
"post": Object {
"current": Object {
"canonical_url": null,
"codeinjection_foot": null,
"codeinjection_head": null,
"comment_id": "62905373e751ff5d4a98db0f",
"created_at": "2022-05-27T04:28:35.000Z",
"custom_excerpt": null,
"custom_template": null,
"email_only": false,
"email_segment": "all",
"email_subject": null,
"excerpt": null,
"feature_image": null,
"feature_image_alt": null,
"feature_image_caption": null,
"featured": false,
"frontmatter": null,
"html": null,
"id": "62905373e751ff5d4a98db0f",
"meta_description": null,
"meta_title": null,
"mobiledoc": "{\\"version\\":\\"0.3.1\\",\\"ghostVersion\\":\\"4.0\\",\\"markups\\":[],\\"atoms\\":[],\\"cards\\":[],\\"sections\\":[[1,\\"p\\",[[0,[],0,\\"\\"]]]]}",
"og_description": null,
"og_image": null,
"og_title": null,
"plaintext": null,
"primary_tag": null,
"published_at": "2022-05-27T04:28:40.000Z",
"slug": "webhookz",
"status": "published",
"tags": Array [],
"tiers": Array [
Object {
"active": true,
"created_at": "2022-05-27T04:28:30.000Z",
"description": null,
"id": "6290536ee751ff5d4a98d92a",
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"type": "paid",
"updated_at": "2022-05-27T04:28:30.000Z",
"visibility": "public",
"welcome_page_url": null,
"yearly_price_id": null,
},
Object {
"active": true,
"created_at": "2022-05-27T04:28:30.000Z",
"description": null,
"id": "6290536ee751ff5d4a98d92b",
"monthly_price_id": null,
"name": "Free",
"slug": "free",
"type": "free",
"updated_at": "2022-05-27T04:28:30.000Z",
"visibility": "public",
"welcome_page_url": null,
"yearly_price_id": null,
},
],
"title": "webhookz",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"updated_at": "2022-05-27T04:28:40.000Z",
"url": "http://127.0.0.1:2369/404/",
"uuid": "e3e0900e-2d00-463f-b83f-c8fa44bea3ae",
"visibility": "public",
},
"previous": Object {
"published_at": null,
"status": "draft",
"tiers": Array [
Object {
"active": true,
"created_at": "2022-05-27T04:28:30.000Z",
"description": null,
"id": "6290536ee751ff5d4a98d92a",
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"type": "paid",
"updated_at": "2022-05-27T04:28:30.000Z",
"visibility": "public",
"welcome_page_url": null,
"yearly_price_id": null,
},
Object {
"active": true,
"created_at": "2022-05-27T04:28:30.000Z",
"description": null,
"id": "6290536ee751ff5d4a98d92b",
"monthly_price_id": null,
"name": "Free",
"slug": "free",
"type": "free",
"updated_at": "2022-05-27T04:28:30.000Z",
"visibility": "public",
"welcome_page_url": null,
"yearly_price_id": null,
},
],
"updated_at": "2022-05-27T04:28:35.000Z",
},
},
}
`;

View file

@ -0,0 +1,52 @@
const {agentProvider, mockManager, fixtureManager} = require('../utils/e2e-framework');
describe('post.* events', function () {
let adminAPIAgent;
let webhookMockReceiver;
before(async function () {
adminAPIAgent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('integrations');
await adminAPIAgent.loginAsOwner();
});
beforeEach(function () {
webhookMockReceiver = mockManager.mockWebhookRequests();
});
afterEach(function () {
mockManager.restore();
});
it('post.published even is triggered', async function () {
await webhookMockReceiver.mock('post.published');
await fixtureManager.insertWebhook({
event: 'post.published',
url: 'https://test-webhook-receiver.com/webhook'
});
const res = await adminAPIAgent
.post('posts/')
.body({
posts: [{
title: 'webhookz',
status: 'draft'
}]
})
.expectStatus(201);
const id = res.body.posts[0].id;
const updatedPost = res.body.posts[0];
updatedPost.status = 'published';
await adminAPIAgent
.put('posts/' + id)
.body({
posts: [updatedPost]
})
.expectStatus(200);
await webhookMockReceiver
.matchBodySnapshot();
});
});

View file

@ -5,6 +5,7 @@ const nock = require('nock');
// Helper services
const configUtils = require('./configUtils');
const WebhookMockReceiver = require('./webhook-mock-receiver');
let mocks = {};
let emailCount = 0;
@ -48,6 +49,12 @@ const mockMail = (response = 'Mail is disabled') => {
return mocks.mail;
};
const mockWebhookRequests = () => {
mocks.webhookMockReceiver = new WebhookMockReceiver();
return mocks.webhookMockReceiver;
};
const sentEmailCount = (count) => {
if (!mocks.mail) {
throw new errors.IncorrectUsageError({
@ -139,6 +146,10 @@ const restore = () => {
emailCount = 0;
nock.cleanAll();
nock.enableNetConnect();
if (mocks.webhookMockReceiver) {
mocks.webhookMockReceiver.reset();
}
};
module.exports = {
@ -148,6 +159,7 @@ module.exports = {
mockStripe,
mockLabsEnabled,
mockLabsDisabled,
mockWebhookRequests,
restore,
assert: {
sentEmailCount,

View file

@ -0,0 +1,91 @@
const nock = require('nock');
const assert = require('assert');
const {snapshotManager} = require('@tryghost/jest-snapshot');
// NOTE: this is a shameless copy-pasta from express-test utils.
// needs to get refactored into reusable package, just like this whole module
const makeMessageFromMatchMessage = (message, errorMessage) => {
const messageLines = message.split('\n');
messageLines.splice(0, 1, errorMessage);
return messageLines.join('\n');
};
class WebhookMockReceiver {
constructor() {
this.bodyResponse;
this.receiver;
this.recordBodyResponse = this.recordBodyResponse.bind(this);
}
recordBodyResponse(body) {
this.bodyResponse = {body};
// let the nock continue with the response
return true;
}
mock() {
this.receiver = nock('https://test-webhook-receiver.com')
.post('/webhook', this.recordBodyResponse)
.reply(200, {status: 'OK'});
return this;
}
reset() {
nock.restore();
this.bodyResponse = undefined;
}
_assertSnapshot(response, assertion) {
const {properties, field, error} = assertion;
if (!response[field]) {
error.message = `Unable to match snapshot on undefined field ${field} ${error.contextString}`;
error.expected = field;
error.actual = 'undefined';
assert.notEqual(response[field], undefined, error);
}
const hint = `[${field}]`;
const match = snapshotManager.match(response[field], properties, hint);
Object.keys(properties).forEach((prop) => {
const errorMessage = `"response.${field}" is missing the expected property "${prop}"`;
error.message = makeMessageFromMatchMessage(match.message(), errorMessage);
error.expected = prop;
error.actual = 'undefined';
error.showDiff = false; // Disable mocha's diff output as it's already present in match.message()
assert.notEqual(response[field][prop], undefined, error);
});
if (match.pass !== true) {
const errorMessage = `"response.${field}" does not match snapshot.`;
error.message = makeMessageFromMatchMessage(match.message(), errorMessage);
error.expected = match.expected;
error.actual = match.actual;
error.showDiff = false; // Disable mocha's diff output as it's already present in match.message()
}
assert.equal(match.pass, true, error);
}
async matchBodySnapshot(properties = {}) {
while (!this.receiver.isDone()) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
let assertion = {
fn: this._assertSnapshot,
properties: properties,
field: 'body',
type: 'body'
};
this._assertSnapshot(this.bodyResponse, assertion);
}
}
module.exports = WebhookMockReceiver;