Skip to content
This repository has been archived by the owner on Dec 11, 2022. It is now read-only.

Commit

Permalink
Release 0.0.8
Browse files Browse the repository at this point in the history
- Added support for creating and deleting connect accounts.
- Removed special connect account `acct_invalid` because any account that has not been created is invalid.
  • Loading branch information
jeff committed Sep 24, 2019
1 parent add454b commit a29a323
Show file tree
Hide file tree
Showing 16 changed files with 364 additions and 94 deletions.
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# stripe-stateful-mock

Simulates a stateful Stripe server for local unit testing. Makes Stripe calls 50-100x faster than testing against the official server. Supports: charging with the most common [test tokens](https://stripe.com/docs/testing), capture, refund, creating and charging customers, adding cards by source token to customers, idempotency and Connect accounts.
Simulates a stateful Stripe server for local unit testing. Makes Stripe calls 50-100x faster than testing against the official server. Supports: charging with the most common [test tokens](https://stripe.com/docs/testing); capture and refund charges; creating and charging customers; adding cards by source token to customers; creating, deleting and using connect accounts; idempotency.

Correctness of this test server is not guaranteed! Set up unit testing to work against either the Stripe server with a test account or this mock server with a flag to switch between them. Test against the official Stripe server occasionally to ensure correctness on the fine details.

Expand Down Expand Up @@ -55,10 +55,6 @@ The first and second time this charge source token is used a 500 response is ret

A random string is appended to the end to guarantee this sequence won't be confused for a similar test (eg `tok_500|tok_500|tok_visa|hjklhjkl`) that may be running simultaneously. It's a lazy hack that accomplishes namespacing.

### Connect account `acct_invalid`

Using the `Stripe-Account` header you can specify a Stripe Connect account. The official server verifies that you are indeed connected to this account. The mock server supports the header but assumes all account IDs are connected and valid. Use the value `acct_invalid` to get a 403 response corresponding to an invalid Connect Account.

## Existing work

[stripe-mock](https://github.com/stripe/stripe-mock) - Stripe's official mock server has better POST body checking but is stateless.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stripe-stateful-mock",
"version": "0.0.7",
"version": "0.0.8",
"description": "A half-baked, stateful Stripe mock server",
"main": "dist/index.js",
"scripts": {
Expand Down
18 changes: 10 additions & 8 deletions src/api/StripeError.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import * as stripe from "stripe";

export interface AdditionalStripeErrorMembers {
charge?: string;
decline_code?: string;
doc_url?: string;
}

export default class StripeError extends Error {
export class StripeError extends Error {

constructor(public statusCode: number, public error: stripe.IStripeError & AdditionalStripeErrorMembers) {
constructor(public statusCode: number, public error: stripe.IStripeError & StripeError.AdditionalStripeErrorMembers) {
super(error.message);
}
}

export namespace StripeError {
export interface AdditionalStripeErrorMembers {
charge?: string;
decline_code?: string;
doc_url?: string;
}
}
163 changes: 163 additions & 0 deletions src/api/accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import * as stripe from "stripe";
import log = require("loglevel");
import {generateId} from "./utils";
import {StripeError} from "./StripeError";

export namespace accounts {

const accounts: {[accountId: string]: stripe.accounts.IAccount} = {};

export function create(accountId: string, params: stripe.accounts.IAccountCreationOptions): stripe.accounts.IAccount {
log.debug("accounts.create", accountId, params);

if (accountId !== "acct_default") {
throw new StripeError(400, {
message: "You can only create new accounts if you've signed up for Connect, which you can learn how to do at https://stripe.com/docs/connect.",
type: "invalid_request_error"
});
}
if (!params.type) {
throw new StripeError(400, {
code: "parameter_missing",
doc_url: "https://stripe.com/docs/error-codes/parameter-missing",
message: "Missing required param: type.",
param: "type",
type: "invalid_request_error"
});
}

const connectedAccountId = `acct_${generateId(16)}`;
const now = new Date();
const account: stripe.accounts.IAccount & any = { // The d.ts is out of date on this object and I don't want to bother.
id: connectedAccountId,
object: "account",
business_profile: {
mcc: (params.business_profile && params.business_profile.mcc) || null,
name: (params.business_profile && params.business_profile.name) || "Stripe.com",
product_description: (params.business_profile && params.business_profile.product_description) || null,
support_address: (params.business_profile && params.business_profile.support_address) || null,
support_email: (params.business_profile && params.business_profile.support_email) || null,
support_phone: (params.business_profile && params.business_profile.support_phone) || null,
support_url: (params.business_profile && params.business_profile.support_url) || null,
url: (params.business_profile && params.business_profile.url) || null
},
business_type: params.business_type || null,
capabilities: {},
charges_enabled: false,
country: params.country || "US",
created: (now.getTime() / 1000) | 0,
default_currency: params.default_currency || "usd",
details_submitted: false,
email: params.email || "site@stripe.com",
external_accounts: {
object: "list",
data: [],
has_more: false,
total_count: 0,
url: `/v1/accounts/${connectedAccountId}/external_accounts`
},
metadata: params.metadata || {},
payouts_enabled: false,
requirements: {
current_deadline: null,
currently_due: [
"business_type",
"business_url",
"company.address.city",
"company.address.line1",
"company.address.postal_code",
"company.address.state",
"person_8UayFKIMRJklog.dob.day",
"person_8UayFKIMRJklog.dob.month",
"person_8UayFKIMRJklog.dob.year",
"person_8UayFKIMRJklog.first_name",
"person_8UayFKIMRJklog.last_name",
"product_description",
"support_phone",
"tos_acceptance.date",
"tos_acceptance.ip"
],
disabled_reason: "requirements.past_due",
eventually_due: [
"business_url",
"product_description",
"support_phone",
"tos_acceptance.date",
"tos_acceptance.ip"
],
past_due: [],
pending_verification: []
},
settings: {
branding: {
icon: (params.settings && params.settings.branding && params.settings.branding.icon) || null,
logo: (params.settings && params.settings.branding && params.settings.branding.logo) || null,
primary_color: (params.settings && params.settings.branding && params.settings.branding.primary_color) || null
},
card_payments: {
decline_on: {
avs_failure: true,
cvc_failure: false
},
statement_descriptor_prefix: null
},
dashboard: {
display_name: "Stripe.com",
timezone: "US/Pacific"
},
payments: {
statement_descriptor: "",
statement_descriptor_kana: null,
statement_descriptor_kanji: null
},
payouts: {
debit_negative_balances: true,
schedule: {
delay_days: 7,
interval: "daily"
},
statement_descriptor: null
}
},
tos_acceptance: {
date: (params.tos_acceptance && params.tos_acceptance.date) || null,
ip: (params.tos_acceptance && params.tos_acceptance.ip) || null,
user_agent: (params.tos_acceptance && params.tos_acceptance.user_agent) || null
},
type: params.type
};
accounts[connectedAccountId] = account;
return account;
}

export function retrieve(accountId: string, connectedAccountId: string, censoredAccessToken: string): stripe.accounts.IAccount {
log.debug("accounts.retrieve", accountId, connectedAccountId);

if (accountId !== "acct_default" && accountId !== connectedAccountId) {
throw new StripeError(400, {
message: "The account specified in the path of /v1/accounts/:account does not match the account specified in the Stripe-Account header.",
type: "invalid_request_error"
});
}
if (!accounts[connectedAccountId]) {
throw new StripeError(403, {
code: "account_invalid",
doc_url: "https://stripe.com/docs/error-codes/account-invalid",
message: `The provided key '${censoredAccessToken}' does not have access to account '${connectedAccountId}' (or that account does not exist). Application access may have been revoked.`,
type: "invalid_request_error"
});
}
return accounts[connectedAccountId];
}

export function del(accountId: string, connectedAccountId: string): stripe.IDeleteConfirmation {
log.debug("accounts.delete", accountId, connectedAccountId);

delete accounts[connectedAccountId];
return {
id: connectedAccountId,
object: "account",
"deleted": true
};
}
}
73 changes: 36 additions & 37 deletions src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,45 @@
import express from "express";
import basicAuthParser = require("basic-auth");
import {getRequestAccountId} from "../routes";

export function authRoute(req: express.Request, res: express.Response, next: express.NextFunction): void {
let token = null;
export namespace auth {
export function authRoute(req: express.Request, res: express.Response, next: express.NextFunction): void {
const token = getAccessTokenFromRequest(req);

const authorizationHeader = req.header("authorization");
const basicAuth = basicAuthParser(req);
if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
token = /^Bearer (.*)/.exec(authorizationHeader)[1];
} else if (basicAuth) {
token = basicAuth.name;
if (!token) {
res.status(401).send({
error: {
message: "You did not provide an API key. You need to provide your API key in the Authorization header, using Bearer auth (e.g. 'Authorization: Bearer YOUR_SECRET_KEY'). See https://stripe.com/docs/api#authentication for details, or we can help at https://support.stripe.com/.",
type: "invalid_request_error"
}
});
} else if (!/^sk_test_/.test(token)) {
res.status(401).send({
error: {
message: `Invalid API Key provided: ${censorAccessToken(token)}`,
type: "invalid_request_error"
}
});
} else {
next();
}
}

if (!token) {
res.status(401).send({
error: {
message: "You did not provide an API key. You need to provide your API key in the Authorization header, using Bearer auth (e.g. 'Authorization: Bearer YOUR_SECRET_KEY'). See https://stripe.com/docs/api#authentication for details, or we can help at https://support.stripe.com/.",
type: "invalid_request_error"
}
});
} else if (!/^sk_test_/.test(token)) {
res.status(401).send({
error: {
message: `Invalid API Key provided: ${censorAccessToken(token)}`,
type: "invalid_request_error"
}
});
} else if (getRequestAccountId(req) === "acct_invalid") {
res.status(403).send({
error: {
code: "account_invalid",
doc_url: "https://stripe.com/docs/error-codes/account-invalid",
message: `The provided key '${censorAccessToken(token)}' does not have access to account '${getRequestAccountId(req)}' (or that account does not exist). Application access may have been revoked.`,
type: "invalid_request_error"
}
});
} else {
next();
export function getAccessTokenFromRequest(req: express.Request): string {
const authorizationHeader = req.header("authorization");
const basicAuth = basicAuthParser(req);
if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
return /^Bearer (.*)/.exec(authorizationHeader)[1];
} else if (basicAuth) {
return basicAuth.name;
}
return null;
}

export function getCensoredAccessTokenFromRequest(req: express.Request): string {
return censorAccessToken(getAccessTokenFromRequest(req));
}
}

function censorAccessToken(token: string): string {
return `${token.substr(0, Math.min(token.length, 11))}${new Array(token.length - Math.min(token.length, 15)).fill("*").join("")}${token.substr(token.length - Math.min(token.length, 4))}`;
export function censorAccessToken(token: string): string {
return `${token.substr(0, Math.min(token.length, 11))}${new Array(token.length - Math.min(token.length, 15)).fill("*").join("")}${token.substr(token.length - Math.min(token.length, 4))}`;
}
}
5 changes: 1 addition & 4 deletions src/api/cards.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as stripe from "stripe";
import log = require("loglevel");
import {generateId} from "./utils";
import {AccountData} from "./AccountData";

namespace cards {
export namespace cards {

export interface CardExtra {
sourceToken: string;
Expand Down Expand Up @@ -141,5 +140,3 @@ namespace cards {
return cardExtras[cardId];
}
}

export default cards;
12 changes: 5 additions & 7 deletions src/api/charges.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as stripe from "stripe";
import log = require("loglevel");
import StripeError from "./StripeError";
import {StripeError} from "./StripeError";
import {generateId, stringifyMetadata} from "./utils";
import {getEffectiveSourceTokenFromChain, isSourceTokenChain} from "./sourceTokenChains";
import cards from "./cards";
import customers from "./customers";
import {cards} from "./cards";
import {AccountData} from "./AccountData";
import disputes from "./disputes";
import {customers} from "./customers";
import {disputes} from "./disputes";

namespace charges {
export namespace charges {

const accountCharges = new AccountData<stripe.charges.ICharge>();

Expand Down Expand Up @@ -646,5 +646,3 @@ namespace charges {
}
}
}

export default charges;
8 changes: 3 additions & 5 deletions src/api/customers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as stripe from "stripe";
import log = require("loglevel");
import StripeError from "./StripeError";
import {StripeError} from "./StripeError";
import {generateId, stringifyMetadata} from "./utils";
import cards from "./cards";
import {cards} from "./cards";
import {AccountData} from "./AccountData";

namespace customers {
export namespace customers {

const accountCustomers = new AccountData<stripe.customers.ICustomer>();

Expand Down Expand Up @@ -260,5 +260,3 @@ namespace customers {
};
}
}

export default customers;
6 changes: 2 additions & 4 deletions src/api/disputes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import * as stripe from "stripe";
import log = require("loglevel");
import {AccountData} from "./AccountData";
import {generateId} from "./utils";
import StripeError from "./StripeError";
import {StripeError} from "./StripeError";

namespace disputes {
export namespace disputes {

const accountDisputes = new AccountData<stripe.disputes.IDispute>();

Expand Down Expand Up @@ -145,5 +145,3 @@ namespace disputes {
return d;
}
}

export default disputes;
2 changes: 1 addition & 1 deletion src/api/idempotency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import express from "express";
import deepEqual = require("deep-equal");
import log = require("loglevel");
import {generateId} from "./utils";
import StripeError from "./StripeError";
import {StripeError} from "./StripeError";
import {AccountData} from "./AccountData";
import {getRequestAccountId} from "../routes";

Expand Down
Loading

0 comments on commit a29a323

Please sign in to comment.