Skip to content

Commit

Permalink
Added basic framework for webhook e2e tests
Browse files Browse the repository at this point in the history
refs https:/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?
  • Loading branch information
naz committed Jun 3, 2022
1 parent 6a3f61f commit 0f4aeaa
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 0 deletions.
113 changes: 113 additions & 0 deletions test/e2e-webhooks/__snapshots__/posts.test.js.snap
Original file line number Diff line number Diff line change
@@ -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",
},
},
}
`;
52 changes: 52 additions & 0 deletions test/e2e-webhooks/posts.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
12 changes: 12 additions & 0 deletions test/utils/e2e-framework-mock-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -139,6 +146,10 @@ const restore = () => {
emailCount = 0;
nock.cleanAll();
nock.enableNetConnect();

if (mocks.webhookMockReceiver) {
mocks.webhookMockReceiver.reset();
}
};

module.exports = {
Expand All @@ -148,6 +159,7 @@ module.exports = {
mockStripe,
mockLabsEnabled,
mockLabsDisabled,
mockWebhookRequests,
restore,
assert: {
sentEmailCount,
Expand Down
91 changes: 91 additions & 0 deletions test/utils/webhook-mock-receiver.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 0f4aeaa

Please sign in to comment.