stripe-stateful-mock
Version:
A half-baked, stateful Stripe mock server
863 lines (741 loc) • 30.3 kB
text/typescript
import Stripe from "stripe";
import chaiExclude from "chai-exclude";
import {getLocalStripeClient} from "./stripeUtils";
import {assertErrorsAreEqual} from "./stripeAssert";
import {generateId} from "../src/api/utils";
import {buildStripeParityTest} from "./buildStripeParityTest";
import chai = require("chai");
chai.use(chaiExclude);
describe("charges", () => {
const localStripeClient = getLocalStripeClient();
const buildChargeParityTest = (params: Stripe.ChargeCreateParams): () => Promise<void> =>
buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create(params);
return [charge];
}
);
const buildChargeFailureParityTest = (params: Stripe.ChargeCreateParams): () => Promise<void> =>
buildStripeParityTest(
async stripeClient => {
let chargeError: any;
try {
await stripeClient.charges.create(params);
} catch (err) {
chargeError = err;
}
return [chargeError];
}
);
it("supports tok_visa", buildChargeParityTest({
amount: 2000,
currency: "usd",
source: "tok_visa"
}));
it("supports tok_visa_debit", buildChargeParityTest({
amount: 2000,
currency: "usd",
source: "tok_visa_debit"
}));
it("supports tok_mastercard", buildChargeParityTest({
amount: 2000,
currency: "usd",
source: "tok_mastercard"
}));
it("supports tok_mastercard_debit", buildChargeParityTest({
amount: 2000,
currency: "usd",
source: "tok_mastercard_debit"
}));
it("supports tok_mastercard_prepaid", buildChargeParityTest({
amount: 2000,
currency: "usd",
source: "tok_mastercard_prepaid"
}));
it("supports tok_amex", buildChargeParityTest({
amount: 2000,
currency: "usd",
source: "tok_amex"
}));
it("supports tok_riskLevelElevated", buildChargeParityTest({
amount: 2000,
currency: "usd",
source: "tok_riskLevelElevated"
}));
it("supports tok_chargeDeclined", buildChargeFailureParityTest({
amount: 2000,
currency: "usd",
source: "tok_chargeDeclined"
}));
it("supports tok_chargeDeclinedInsufficientFunds", buildChargeFailureParityTest({
amount: 2000,
currency: "usd",
source: "tok_chargeDeclinedInsufficientFunds"
}));
it("supports tok_chargeDeclinedFraudulent", buildChargeFailureParityTest({
amount: 2000,
currency: "usd",
source: "tok_chargeDeclinedFraudulent"
}));
it("supports tok_chargeDeclinedIncorrectCvc", buildChargeFailureParityTest({
amount: 2000,
currency: "usd",
source: "tok_chargeDeclinedIncorrectCvc"
}));
it("supports tok_chargeDeclinedExpiredCard", buildChargeFailureParityTest({
amount: 2000,
currency: "usd",
source: "tok_chargeDeclinedExpiredCard"
}));
it("supports tok_chargeDeclinedProcessingError", buildChargeFailureParityTest({
amount: 2000,
currency: "usd",
source: "tok_chargeDeclinedProcessingError"
}));
it("supports tok_chargeDeclinedProcessingError", buildChargeFailureParityTest({
amount: 2000,
currency: "usd",
source: "tok_chargeDeclinedProcessingError"
}));
it("supports tok_createDispute", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 5000,
currency: "usd",
source: "tok_createDispute"
});
chai.assert.isNull(charge.dispute);
// Because the dispute is created after the charge is returned there's a race condition.
await new Promise((resolve => setTimeout(resolve)));
const chargeGet = await stripeClient.charges.retrieve(charge.id);
chai.assert.isString(chargeGet.dispute);
return [charge, chargeGet];
}
));
it("supports tok_createDisputeProductNotReceived", buildStripeParityTest(
async (stripeClient, mode) => {
const charge = await stripeClient.charges.create({
amount: 5000,
currency: "usd",
source: "tok_createDisputeProductNotReceived"
});
chai.assert.isNull(charge.dispute, mode);
// Because the dispute is created after the charge is returned there's a race condition.
await new Promise((resolve => setTimeout(resolve)));
const chargeGet = await stripeClient.charges.retrieve(charge.id);
chai.assert.isString(chargeGet.dispute, mode);
return [charge, chargeGet];
}
));
it("supports tok_createDisputeInquiry", buildStripeParityTest(
async (stripeClient, mode) => {
const charge = await stripeClient.charges.create({
amount: 5000,
currency: "usd",
source: "tok_createDisputeInquiry"
});
chai.assert.isNull(charge.dispute, mode);
// Because the dispute is created after the charge is returned there's a race condition.
await new Promise((resolve => setTimeout(resolve)));
const chargeGet = await stripeClient.charges.retrieve(charge.id);
chai.assert.isString(chargeGet.dispute, mode);
return [charge, chargeGet];
}
));
it("supports metadata", buildChargeParityTest({
amount: 8888,
currency: "usd",
source: "tok_visa",
metadata: {
selfReferential: "yes",
testNumberHandling: 420
}
}));
it("supports capture=false", buildStripeParityTest(
async (stripeClient, mode) => {
const charge = await stripeClient.charges.create({
amount: 3500,
currency: "usd",
source: "tok_visa",
capture: false
});
chai.assert.isFalse(charge.captured, mode);
chai.assert.isNull(charge.balance_transaction, mode);
return [charge];
}
));
it("supports on_behalf_of", buildChargeParityTest({
amount: 3500,
currency: "usd",
on_behalf_of: process.env["STRIPE_CONNECTED_ACCOUNT_ID"],
source: "tok_visa",
transfer_group: "xfer"
}));
it("supports misc additional params", buildChargeParityTest({
amount: 3500,
description: "this is a description",
currency: "usd",
receipt_email: "foobar@example.com",
shipping: {
address: {
city: "Beverly Hills",
country: "US",
line1: "1675 E. Altadena Drive",
line2: null,
postal_code: "90210",
state: "CA"
},
carrier: null,
name: "Henrietta",
phone: null,
tracking_number: "abc123"
},
source: "tok_visa",
statement_descriptor: "hello world",
}));
it("supports upper case currency", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 3500,
currency: "USD",
source: "tok_visa"
});
chai.assert.equal(charge.currency, "usd");
return [charge];
}
));
it("checks required amount", buildChargeFailureParityTest({
currency: "usd",
source: "tok_visa"
} as any));
it("checks required currency", buildChargeFailureParityTest({
amount: 5000,
source: "tok_visa"
} as any));
it("checks amount is positive", buildChargeFailureParityTest({
amount: -1,
currency: "usd",
source: "tok_visa"
}));
it("checks min amount", buildChargeFailureParityTest({
amount: 5,
currency: "usd",
source: "tok_visa"
}));
it("checks max amount", buildChargeFailureParityTest({
amount: 1000000000,
currency: "usd",
source: "tok_visa"
}));
it("supports Stripe-Account header (Connect account)", buildStripeParityTest(
async (stripeClient, mode) => {
let connectedAccountId: string;
if (mode === "live") {
chai.assert.isString(process.env["STRIPE_CONNECTED_ACCOUNT_ID"], "connected account ID is set");
connectedAccountId = process.env["STRIPE_CONNECTED_ACCOUNT_ID"];
} else {
const account = await stripeClient.accounts.create({type: "custom"});
connectedAccountId = account.id;
}
const charge = await stripeClient.charges.create(
{
amount: 2000,
currency: "usd",
source: "tok_visa"
},
{
stripeAccount: connectedAccountId
}
);
let retrieveError: any;
try {
await stripeClient.charges.retrieve(charge.id);
} catch (err) {
retrieveError = err;
}
chai.assert.isDefined(retrieveError, "charge should not be in the account, but should be in the connected account");
const connectRetrieveCharge = await stripeClient.charges.retrieve(charge.id, {stripeAccount: connectedAccountId});
chai.assert.deepEqual(connectRetrieveCharge, charge);
return [charge, retrieveError, connectRetrieveCharge];
}
));
it("can list charges", async () => {
// Create a fresh account to get a clean slate.
const account = await localStripeClient.accounts.create({type: "custom"});
const listEmpty = await localStripeClient.customers.list({stripeAccount: account.id});
chai.assert.lengthOf(listEmpty.data, 0);
const charge0 = await localStripeClient.charges.create({
amount: 1234,
currency: "usd",
source: "tok_visa"
}, {stripeAccount: account.id});
const listOne = await localStripeClient.charges.list({stripeAccount: account.id});
chai.assert.lengthOf(listOne.data, 1);
chai.assert.sameDeepMembers(listOne.data, [charge0]);
const charge1 = await localStripeClient.charges.create({
amount: 5678,
currency: "usd",
source: "tok_visa"
}, {stripeAccount: account.id});
const listTwo = await localStripeClient.charges.list({stripeAccount: account.id});
chai.assert.lengthOf(listTwo.data, 2);
chai.assert.sameDeepMembers(listTwo.data, [charge1, charge0]);
const listLimit1 = await localStripeClient.charges.list({limit: 1}, {stripeAccount: account.id});
chai.assert.lengthOf(listLimit1.data, 1);
const listLimit2 = await localStripeClient.charges.list({
limit: 1,
starting_after: listLimit1.data[0].id
}, {stripeAccount: account.id});
chai.assert.lengthOf(listLimit1.data, 1);
chai.assert.sameDeepMembers([...listLimit2.data, ...listLimit1.data], listTwo.data);
});
describe("unofficial token support", () => {
describe("tok_429", () => {
it("throws a 429 error", async () => {
let error: any;
try {
await localStripeClient.charges.create({
amount: 5000,
currency: "usd",
source: "tok_429"
});
} catch (err) {
error = err;
}
chai.assert.isDefined(error);
chai.assert.equal(error.statusCode, 429);
chai.assert.equal(error.rawType, "rate_limit_error");
chai.assert.equal(error.type, "StripeRateLimitError");
});
});
describe("tok_500", () => {
it("throws a 500 error", async () => {
let error: any;
try {
await localStripeClient.charges.create({
amount: 5000,
currency: "usd",
source: "tok_500"
});
} catch (err) {
error = err;
}
chai.assert.isDefined(error);
chai.assert.equal(error.statusCode, 500);
chai.assert.equal(error.rawType, "api_error");
chai.assert.equal(error.type, "StripeAPIError");
});
});
describe("tok_forget", () => {
it("creates a successful charge that is not saved", async () => {
const charge = await localStripeClient.charges.create(
{
amount: 2000,
currency: "usd",
source: "tok_forget"
}
);
chai.assert.isString(charge.id);
let getChargeError: any;
try {
await localStripeClient.charges.retrieve(charge.id);
} catch (err) {
getChargeError = err;
}
chai.assert.isDefined(getChargeError);
chai.assert.equal(getChargeError.statusCode, 404);
});
});
describe("source token chains", async () => {
it("supports test case tok_chargeDeclinedInsufficientFunds|tok_visa", async () => {
const chargeParams: Stripe.ChargeCreateParams = {
amount: 5000,
currency: "usd",
source: "tok_chargeDeclinedInsufficientFunds|tok_visa"
};
let nsfError: any;
try {
await localStripeClient.charges.create(chargeParams);
} catch (err) {
nsfError = err;
}
chai.assert.isDefined(nsfError);
chai.assert.equal(nsfError.type, "StripeCardError");
const charge = await localStripeClient.charges.create(chargeParams);
chai.assert.equal(charge.amount, chargeParams.amount);
});
it("supports test case tok_500|tok_500|tok_visa", async () => {
const chargeParams: Stripe.ChargeCreateParams = {
amount: 5000,
currency: "usd",
source: "tok_500|tok_500|tok_visa"
};
let error1: any;
try {
await localStripeClient.charges.create(chargeParams);
} catch (err) {
error1 = err;
}
chai.assert.isDefined(error1);
chai.assert.equal(error1.statusCode, 500);
chai.assert.equal(error1.type, "StripeAPIError");
let error2: any;
try {
await localStripeClient.charges.create(chargeParams);
} catch (err) {
error2 = err;
}
chai.assert.isDefined(error2);
chai.assert.equal(error2.statusCode, 500);
chai.assert.equal(error2.type, "StripeAPIError");
const charge = await localStripeClient.charges.create(chargeParams);
chai.assert.equal(charge.amount, chargeParams.amount);
});
it("does not confuse 2 chains that are not identical", async () => {
const chargeParams1: Stripe.ChargeCreateParams = {
amount: 5000,
currency: "usd",
source: `tok_500|tok_visa|${generateId(8)}`
};
const chargeParams2: Stripe.ChargeCreateParams = {
amount: 5000,
currency: "usd",
source: `tok_500|tok_visa|${generateId(8)}`
};
let error1: any;
try {
await localStripeClient.charges.create(chargeParams1);
} catch (err) {
error1 = err;
}
chai.assert.isDefined(error1);
chai.assert.equal(error1.statusCode, 500);
chai.assert.equal(error1.type, "StripeAPIError");
let error2: any;
try {
await localStripeClient.charges.create(chargeParams2);
} catch (err) {
error2 = err;
}
chai.assert.isDefined(error2);
chai.assert.equal(error2.statusCode, 500);
chai.assert.equal(error2.type, "StripeAPIError");
const charge1 = await localStripeClient.charges.create(chargeParams1);
chai.assert.equal(charge1.amount, chargeParams1.amount);
const charge2 = await localStripeClient.charges.create(chargeParams2);
chai.assert.equal(charge2.amount, chargeParams2.amount);
});
});
});
describe("idempotency", () => {
it("replays idempotent successes", async () => {
const idempotencyKey = generateId();
const params: Stripe.ChargeCreateParams = {
amount: 50000,
currency: "usd",
source: "tok_visa"
};
const originalCharge = await localStripeClient.charges.create(params, {idempotencyKey: idempotencyKey});
const repeatCharge = await localStripeClient.charges.create(params, {idempotencyKey: idempotencyKey});
chai.assert.deepEqual(repeatCharge, originalCharge);
});
it("replays idempotent errors", async () => {
const idempotencyKey = generateId();
const params: Stripe.ChargeCreateParams = {
amount: 5,
currency: "usd",
source: "tok_visa"
};
let originalError: any;
try {
await localStripeClient.charges.create(params, {idempotencyKey: idempotencyKey});
} catch (err) {
originalError = err;
}
chai.assert.isDefined(originalError);
let repeatError: any;
try {
await localStripeClient.charges.create(params, {idempotencyKey: idempotencyKey});
} catch (err) {
repeatError = err;
}
chai.assert.isDefined(repeatError);
assertErrorsAreEqual(repeatError, originalError);
chai.assert.equal(repeatError.headers["original-request"], originalError.headers["request-id"]);
chai.assert.equal(repeatError.headers["idempotent-replayed"], "true");
});
it("replays 500s (yes Stripe really does that)", async () => {
const params: Stripe.ChargeCreateParams = {
amount: 5, // This amount is too small but tok_500 takes precedence.
currency: "usd",
source: "tok_500"
};
const idempotencyKey = generateId();
let originalError: any;
try {
await localStripeClient.charges.create(params, {
idempotencyKey: idempotencyKey
});
} catch (err) {
originalError = err;
}
chai.assert.isDefined(originalError);
chai.assert.equal(originalError.type, "StripeAPIError");
let repeatError: any;
try {
await localStripeClient.charges.create(params, {
idempotencyKey: idempotencyKey
});
} catch (err) {
repeatError = err;
}
chai.assert.isDefined(repeatError);
assertErrorsAreEqual(repeatError, originalError);
chai.assert.equal(repeatError.headers["original-request"], originalError.headers["request-id"]);
});
it("sends the right error on mismatched idempotent bodies", async () => {
const idempotencyKey = generateId();
await localStripeClient.charges.create({
amount: 1000,
currency: "usd",
source: "tok_visa"
}, {
idempotencyKey: idempotencyKey
});
let repeatError: any;
try {
await localStripeClient.charges.create({
amount: 2000,
currency: "usd",
source: "tok_visa"
}, {
idempotencyKey: idempotencyKey
});
} catch (err) {
repeatError = err;
}
chai.assert.isDefined(repeatError);
chai.assert.equal(repeatError.statusCode, 400);
chai.assert.equal(repeatError.rawType, "idempotency_error");
chai.assert.equal(repeatError.type, "StripeIdempotencyError");
});
it("does not confused two Connected accounts", async () => {
const idempotencyKey = generateId();
const account1 = await localStripeClient.accounts.create({type: "custom"});
const account2 = await localStripeClient.accounts.create({type: "custom"});
await localStripeClient.charges.create({
amount: 1000,
currency: "usd",
source: "tok_visa"
}, {
idempotencyKey: idempotencyKey,
stripeAccount: account1.id
});
await localStripeClient.charges.create({
amount: 2000,
currency: "usd",
source: "tok_visa"
}, {
idempotencyKey: idempotencyKey,
stripeAccount: account2.id
});
});
});
describe("pending capture", () => {
it("fully captures by default", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 7200,
currency: "usd",
source: "tok_visa",
capture: false
});
chai.assert.isFalse(charge.captured);
const capture = await stripeClient.charges.capture(charge.id);
chai.assert.isTrue(capture.captured);
chai.assert.lengthOf(capture.refunds.data, 0);
return [charge, capture];
}
));
it("fully captures manually entered amount", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 7200,
currency: "usd",
source: "tok_visa",
capture: false
});
chai.assert.isFalse(charge.captured);
const capture = await stripeClient.charges.capture(charge.id, {amount: 7200});
chai.assert.isTrue(capture.captured);
chai.assert.lengthOf(capture.refunds.data, 0);
return [charge, capture];
}
));
it("partially captures", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 7200,
currency: "usd",
source: "tok_visa",
capture: false
});
chai.assert.isFalse(charge.captured);
const capture = await stripeClient.charges.capture(charge.id, {amount: 3200});
chai.assert.isTrue(capture.captured);
chai.assert.lengthOf(capture.refunds.data, 1);
return [charge, capture];
}
));
it("can't capture twice", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 7200,
currency: "usd",
source: "tok_visa",
capture: false
});
chai.assert.isFalse(charge.captured);
const capture = await stripeClient.charges.capture(charge.id, {amount: 3200});
chai.assert.isTrue(capture.captured);
chai.assert.lengthOf(capture.refunds.data, 1);
let capture2Error: any;
try {
await stripeClient.charges.capture(charge.id, {amount: 3200});
} catch (err) {
capture2Error = err;
}
return [charge, capture, capture2Error];
}
));
it("can't capture less than the min charge", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 7200,
currency: "usd",
source: "tok_visa",
capture: false
});
chai.assert.isFalse(charge.captured);
let captureError: any;
try {
await stripeClient.charges.capture(charge.id, {amount: 12});
} catch (err) {
captureError = err;
}
return [charge, captureError];
}
));
it("can't capture a non-existent charge", buildStripeParityTest(
async stripeClient => {
let captureError: any;
try {
await stripeClient.charges.capture(generateId());
} catch (err) {
captureError = err;
}
return [captureError];
}
));
});
describe("pending void", () => {
it("fully voids by default", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 7200,
currency: "usd",
source: "tok_visa",
capture: false
});
chai.assert.isFalse(charge.captured);
const voyed = await stripeClient.refunds.create({charge: charge.id});
return [charge, voyed];
}
));
it("fully voids manually entered amount", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 7200,
currency: "usd",
source: "tok_visa",
capture: false
});
chai.assert.isFalse(charge.captured);
const voyed = await stripeClient.refunds.create({charge: charge.id, amount: 7200});
return [charge, voyed];
}
));
it("stores void metadata", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 7200,
currency: "usd",
source: "tok_visa",
capture: false
});
chai.assert.isFalse(charge.captured);
const voyed = await stripeClient.refunds.create({
charge: charge.id,
metadata: {
extra: "info"
}
});
chai.assert.isObject(voyed.metadata);
return [charge, voyed];
}
));
it("can't partially void", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 7200,
currency: "usd",
source: "tok_visa",
capture: false
});
let voidError: any;
try {
await stripeClient.refunds.create({charge: charge.id, amount: 1200});
} catch (err) {
voidError = err;
}
return [charge, voidError];
}
));
});
describe("update", () => {
it("can update metadata", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 4300,
currency: "usd",
source: "tok_visa",
metadata: {
a: "alpha"
}
});
const updatedCharge = await stripeClient.charges.update(charge.id, {
metadata: {
a: "aardvark",
b: "boa"
}
});
chai.assert.notDeepEqual(updatedCharge.metadata, charge.metadata);
return [charge, updatedCharge];
}
));
it("can update misc params", buildStripeParityTest(
async stripeClient => {
const charge = await stripeClient.charges.create({
amount: 4300,
currency: "usd",
source: "tok_visa"
});
const updatedCharge = await stripeClient.charges.update(charge.id, {
receipt_email: "receipt@example.com",
description: "a new description"
});
return [charge, updatedCharge];
}
));
});
});