Skip to content

Commit

Permalink
Move generateTestHeaderString to stripe.webhooks (#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulasjes-stripe authored and rattrayalex-stripe committed May 8, 2019
1 parent 077bf84 commit e30553b
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 65 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,9 @@ const payload = {
const payloadString = JSON.stringify(payload, null, 2);
const secret = 'whsec_test_secret';

const header = stripe.generateWebhookHeaderString({
const header = stripe.webhooks.generateTestHeaderString({
payload: payloadString,
secret,
});

const event = stripe.webhooks.constructEvent(payloadString, header, secret);
Expand Down
31 changes: 31 additions & 0 deletions lib/Webhooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,37 @@ var Webhook = {
var jsonPayload = JSON.parse(payload);
return jsonPayload;
},

/**
* Generates a header to be used for webhook mocking
*
* @typedef {object} opts
* @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
*/
generateTestHeaderString: function(opts) {
if (!opts) {
throw new Error.StripeError({
message: 'Options are required',
});
}

opts.timestamp = Math.floor(opts.timestamp) || Math.floor(Date.now() / 1000);
opts.scheme = opts.scheme || signature.EXPECTED_SCHEME;

opts.signature = opts.signature ||
signature._computeSignature(opts.timestamp + '.' + opts.payload, opts.secret);

var generatedHeader = [
't=' + opts.timestamp,
opts.scheme + '=' + opts.signature,
].join(',');

return generatedHeader;
},
};

var signature = {
Expand Down
6 changes: 0 additions & 6 deletions lib/stripe.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ 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');

Expand Down Expand Up @@ -341,11 +340,6 @@ 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);
Expand Down
65 changes: 50 additions & 15 deletions test/Webhook.spec.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
'use strict';

var testUtils = require('../testUtils');
var stripe = testUtils.getSpyableStripe();
var generateHeaderString = testUtils.generateHeaderString;
var stripe = require('../testUtils').getSpyableStripe();
var expect = require('chai').expect;
var Buffer = require('safe-buffer').Buffer;

var EVENT_PAYLOAD = testUtils.EVENT_PAYLOAD;
var EVENT_PAYLOAD_STRING = testUtils.EVENT_PAYLOAD_STRING;
var SECRET = testUtils.WEBHOOK_SECRET;
var EVENT_PAYLOAD = {
id: 'evt_test_webhook',
object: 'event',
};
var EVENT_PAYLOAD_STRING = JSON.stringify(EVENT_PAYLOAD, null, 2);
var SECRET = 'whsec_test_secret';

describe('Webhooks', function() {
describe('.generateTestHeaderString', function() {
it('should throw when no opts are passed', function() {
expect(function() {
stripe.webhooks.generateTestHeaderString()
}).to.throw();
});

it('should correctly construct a webhook header', function() {
var header = stripe.webhooks.generateTestHeaderString({
payload: EVENT_PAYLOAD_STRING,
secret: SECRET,
});

expect(header).to.not.be.undefined;
expect(header.split(',')).to.have.lengthOf(2);
});
});

describe('.constructEvent', function() {
it('should return an Event instance from a valid JSON payload and valid signature header', function() {
var header = generateHeaderString({
var header = stripe.webhooks.generateTestHeaderString({
payload: EVENT_PAYLOAD_STRING,
secret: SECRET,
});

var event = stripe.webhooks.constructEvent(EVENT_PAYLOAD_STRING, header, SECRET);
Expand All @@ -24,8 +44,9 @@ describe('Webhooks', function() {

it('should raise a JSON error from invalid JSON payload',
function() {
var header = generateHeaderString({
var header = stripe.webhooks.generateTestHeaderString({
payload: '} I am not valid JSON; 123][',
secret: SECRET,
});
expect(function() {
stripe.webhooks.constructEvent('} I am not valid JSON; 123][', header, SECRET);
Expand Down Expand Up @@ -66,7 +87,9 @@ describe('Webhooks', function() {
});

it('should raise a SignatureVerificationError when there are no signatures with the expected scheme', function() {
var header = generateHeaderString({
var header = stripe.webhooks.generateTestHeaderString({
payload: EVENT_PAYLOAD_STRING,
secret: SECRET,
scheme: 'v0',
});

Expand All @@ -76,7 +99,9 @@ describe('Webhooks', function() {
});

it('should raise a SignatureVerificationError when there are no valid signatures for the payload', function() {
var header = generateHeaderString({
var header = stripe.webhooks.generateTestHeaderString({
payload: EVENT_PAYLOAD_STRING,
secret: SECRET,
signature: 'bad_signature',
});

Expand All @@ -86,8 +111,10 @@ describe('Webhooks', function() {
});

it('should raise a SignatureVerificationError when the timestamp is not within the tolerance', function() {
var header = generateHeaderString({
var header = stripe.webhooks.generateTestHeaderString({
timestamp: (Date.now() / 1000) - 15,
payload: EVENT_PAYLOAD_STRING,
secret: SECRET,
});

expect(function() {
Expand All @@ -98,16 +125,20 @@ describe('Webhooks', function() {
it('should return true when the header contains a valid signature and ' +
'the timestamp is within the tolerance',
function() {
var header = generateHeaderString({
var header = stripe.webhooks.generateTestHeaderString({
timestamp: (Date.now() / 1000),
payload: EVENT_PAYLOAD_STRING,
secret: SECRET,
});

expect(stripe.webhooks.signature.verifyHeader(EVENT_PAYLOAD_STRING, header, SECRET, 10)).to.equal(true);
});

it('should return true when the header contains at least one valid signature', function() {
var header = generateHeaderString({
var header = stripe.webhooks.generateTestHeaderString({
timestamp: (Date.now() / 1000),
payload: EVENT_PAYLOAD_STRING,
secret: SECRET,
});

header += ',v1=potato';
Expand All @@ -118,16 +149,20 @@ describe('Webhooks', function() {
it('should return true when the header contains a valid signature ' +
'and the timestamp is off but no tolerance is provided',
function() {
var header = generateHeaderString({
var header = stripe.webhooks.generateTestHeaderString({
timestamp: 12345,
payload: EVENT_PAYLOAD_STRING,
secret: SECRET,
});

expect(stripe.webhooks.signature.verifyHeader(EVENT_PAYLOAD_STRING, header, SECRET)).to.equal(true);
});

it('should accept Buffer instances for the payload and header', function() {
var header = generateHeaderString({
var header = stripe.webhooks.generateTestHeaderString({
timestamp: (Date.now() / 1000),
payload: EVENT_PAYLOAD_STRING,
secret: SECRET,
});

expect(stripe.webhooks.signature.verifyHeader(Buffer.from(EVENT_PAYLOAD_STRING), Buffer.from(header), SECRET, 10)).to.equal(true);
Expand Down
43 changes: 0 additions & 43 deletions testUtils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,6 @@ require('chai').use(require('chai-as-promised'));
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';

Expand Down Expand Up @@ -168,35 +156,4 @@ var utils = module.exports = {
return false;
}
},

/**
* 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;
},
};

0 comments on commit e30553b

Please sign in to comment.