diff --git a/.eslintignore b/.eslintignore index e21c628a03..a6de844ce0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ *.node*.js +node_modules \ No newline at end of file diff --git a/README.md b/README.md index af66e56c8f..8bfc261d52 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,29 @@ const event = stripe.webhooks.constructEvent( ); ``` +#### Testing Webhook signing + +You can use `generateWebhookHeaderString` to mock webhook events that come from Stripe: + +```js +const payload = { + id: 'evt_test_webhook', + object: 'event', +}; + +const payloadString = JSON.stringify(payload, null, 2); +const secret = 'whsec_test_secret'; + +const header = stripe.generateWebhookHeaderString({ + payload: payloadString, +}); + +const event = stripe.webhooks.constructEvent(payloadString, header, secret); + +// Do something with mocked signed event +expect(event.id).to.equal(payload.id); +``` + ### Writing a Plugin If you're writing a plugin that uses the library, we'd appreciate it if you identified using `stripe.setAppInfo()`: diff --git a/lib/stripe.js b/lib/stripe.js index fa5d96f51b..4c6a24826e 100644 --- a/lib/stripe.js +++ b/lib/stripe.js @@ -28,6 +28,7 @@ var APP_INFO_PROPERTIES = ['name', 'version', 'url', 'partner_id']; var EventEmitter = require('events').EventEmitter; var utils = require('./utils'); +var testUtils = require('../testUtils'); var resourceNamespace = require('./ResourceNamespace'); @@ -340,6 +341,11 @@ Stripe.prototype = { return this._enableTelemetry; }, + // Generates a header to be used for webhook mocking + generateWebhookHeaderString: function(opts) { + return testUtils.generateHeaderString.apply(testUtils, opts); + }, + _prepResources: function() { for (var name in resources) { this[utils.pascalToCamelCase(name)] = new resources[name](this); diff --git a/test/Webhook.spec.js b/test/Webhook.spec.js index 9576888d59..8950574742 100644 --- a/test/Webhook.spec.js +++ b/test/Webhook.spec.js @@ -1,16 +1,14 @@ 'use strict'; -var stripe = require('../testUtils').getSpyableStripe(); +var testUtils = require('../testUtils'); +var stripe = testUtils.getSpyableStripe(); +var generateHeaderString = testUtils.generateHeaderString; var expect = require('chai').expect; var Buffer = require('safe-buffer').Buffer; -var EVENT_PAYLOAD = { - id: 'evt_test_webhook', - object: 'event', -}; -var EVENT_PAYLOAD_STRING = JSON.stringify(EVENT_PAYLOAD, null, 2); - -var SECRET = 'whsec_test_secret'; +var EVENT_PAYLOAD = testUtils.EVENT_PAYLOAD; +var EVENT_PAYLOAD_STRING = testUtils.EVENT_PAYLOAD_STRING; +var SECRET = testUtils.WEBHOOK_SECRET; describe('Webhooks', function() { describe('.constructEvent', function() { @@ -136,22 +134,3 @@ describe('Webhooks', function() { }); }); }); - -function generateHeaderString(opts) { - opts = opts || {}; - - opts.timestamp = Math.floor(opts.timestamp) || Math.floor(Date.now() / 1000); - opts.payload = opts.payload || EVENT_PAYLOAD_STRING; - opts.secret = opts.secret || SECRET; - opts.scheme = opts.scheme || stripe.webhooks.signature.EXPECTED_SCHEME; - - opts.signature = opts.signature || - stripe.webhooks.signature._computeSignature(opts.timestamp + '.' + opts.payload, opts.secret); - - var generatedHeader = [ - 't=' + opts.timestamp, - opts.scheme + '=' + opts.signature, - ].join(','); - - return generatedHeader; -} diff --git a/testUtils/index.js b/testUtils/index.js index 9d885ff456..1517645fa2 100644 --- a/testUtils/index.js +++ b/testUtils/index.js @@ -10,6 +10,17 @@ var ResourceNamespace = require('../lib/ResourceNamespace').ResourceNamespace; var utils = module.exports = { + EVENT_PAYLOAD: { + id: 'evt_test_webhook', + object: 'event', + }, + + get EVENT_PAYLOAD_STRING() { + return JSON.stringify(this.EVENT_PAYLOAD, null, 2); + }, + + WEBHOOK_SECRET: 'whsec_test_secret', + getUserStripeKey: function() { var key = process.env.STRIPE_TEST_API_KEY || 'tGN0bIwXnHdwOa85VABjPdSn8nWY7G7I'; @@ -158,4 +169,34 @@ var utils = module.exports = { } }, + /** + * Generates a header to be used for webhook mocking + * + * @typedef {object} opts + * @property {object} stripe - Instance of Stripe to use. Defaults to a spyable instance for testing. + * @property {number} timestamp - Timestamp of the header. Defaults to Date.now() + * @property {string} payload - JSON stringified payload object, containing the 'id' and 'object' parameters + * @property {string} secret - Stripe webhook secret 'whsec_...' + * @property {string} scheme - Version of API to hit. Defaults to 'v1'. + * @property {string} signature - Computed webhook signature + */ + generateHeaderString: function(opts) { + opts = opts || {}; + + opts.stripe = opts.stripe || utils.getSpyableStripe(); + opts.timestamp = Math.floor(opts.timestamp) || Math.floor(Date.now() / 1000); + opts.payload = opts.payload || utils.EVENT_PAYLOAD_STRING; + opts.secret = opts.secret || utils.WEBHOOK_SECRET; + opts.scheme = opts.scheme || opts.stripe.webhooks.signature.EXPECTED_SCHEME; + + opts.signature = opts.signature || + opts.stripe.webhooks.signature._computeSignature(opts.timestamp + '.' + opts.payload, opts.secret); + + var generatedHeader = [ + 't=' + opts.timestamp, + opts.scheme + '=' + opts.signature, + ].join(','); + + return generatedHeader; + }, };