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

Commit

Permalink
Merge pull request #5 from Giftbit/RequiredParams
Browse files Browse the repository at this point in the history
Release 0.0.12
  • Loading branch information
Jeffery Grajkowski committed Nov 6, 2019
2 parents ad659f2 + 08f2823 commit 48c68a3
Show file tree
Hide file tree
Showing 28 changed files with 765 additions and 254 deletions.
3 changes: 0 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,3 @@ STRIPE_TEST_SECRET_KEY=

# A Stripe account that is already connected to the account above.
STRIPE_CONNECTED_ACCOUNT_ID=

# A stripe test plan id is necessary to run tests related to subscriptions.
STRIPE_TEST_PLAN_ID=
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
# 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 and refund charges; creating and charging customers; adding cards by source token to customers; creating, deleting and using connect accounts; idempotency.
Simulates a stateful Stripe server for local unit testing. Makes Stripe calls 50-100x faster than testing against the official server.

Supported features:
- charges: create with the most common [test tokens](https://stripe.com/docs/testing) or customer card, retrieve, list, update, capture
- refunds: create, retrieve, list
- customers: create, retrieve, list, update, create card, retrieve card, delete card
- products: create, retrieve, list
- plans: create, retrieve, list
- subscriptions: create, retrieve, list
- connect accounts: create and delete
- 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.

## Usage

`node stripe-stateful-mock`

Starts an HTTP server (default port is 8000). This can be connected to with any Stripe client. For example in JavaScript...
The server can be started in multiple ways but in each case it starts an HTTP server (default port is 8000) which can be connected to with any Stripe client. For example in JavaScript...

```javascript
const Stripe = require("stripe");
Expand All @@ -21,6 +29,32 @@ The server supports the following settings through environment variables:
- `LOG_LEVEL` sets the log output verbosity. Values are: `silent`, `error`, `warn`, `info`, `debug`.
- `PORT` the port to start the server on. Defaults to 8000.

### As a shell command

Install globally: `npm i -g stripe-stateful-mock`

Run: `node stripe-stateful-mock`

### As part of a Mocha unit test

Install as a development dependency: `npm i -D stripe-stateful-mock`

In your NPM test script: `mocha --require stripe-stateful-mock/autostart`

### Custom

The server is implemented as an Express 4 application and can be fully controlled. This does not start the server by default.

```javascript
const http = require("http");
const https = require("https");
const stripeStatefulMock = require("stripe-stateful-mock");

const app = stripeStatefulMock.createExpressApp();
http.createServer(app).listen(80);
https.createServer(options, app).listen(443);
```

## Bonus features

This server supports a few bonus parameters to test scenarios not testable against the real server.
Expand Down
5 changes: 5 additions & 0 deletions autostart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// This script starts the server automatically using env vars to control configuration.
// eg: require("stripe-stateful-mock/autostart")
// It lives outside `src` so it can be required without including `dist` in the path.

require("./dist/autostart");
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
{
"name": "stripe-stateful-mock",
"version": "0.0.11",
"version": "0.0.12",
"description": "A half-baked, stateful Stripe mock server",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"clean": "rimraf ./dist",
"lint": "tslint --project tsconfig.json",
"prepublishOnly": "npm run clean && npm run build && npm run lint && npm run test",
"run": "LOG_LEVEL=info node dist/index.js",
"run:debug": "LOG_LEVEL=debug node dist/index.js",
"test": "npm run lint && LOG_LEVEL=silent mocha --recursive --timeout 5000 --require ts-node/register --require ./test/requireDotEnv.ts --exit \"test/**/*.ts\"",
"test:debug": "LOG_LEVEL=trace mocha --recursive --timeout 5000 --require ts-node/register --require ./test/requireDotEnv.ts --exit \"test/**/*.ts\""
"run": "LOG_LEVEL=${LOG_LEVEL:=info} node dist/index.js",
"run:debug": "LOG_LEVEL=debug npm run run",
"test": "npm run lint && LOG_LEVEL=${LOG_LEVEL:=silent} mocha --recursive --timeout 5000 --require ts-node/register --require ./test/requireDotEnv.ts --exit \"test/**/*.ts\"",
"test:debug": "LOG_LEVEL=trace npm run test"
},
"bin": "./dist/cli.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Giftbit/stripe-stateful-mock.git"
Expand Down
13 changes: 0 additions & 13 deletions server.js

This file was deleted.

11 changes: 2 additions & 9 deletions src/api/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as stripe from "stripe";
import log = require("loglevel");
import {applyListOptions, generateId} from "./utils";
import {StripeError} from "./StripeError";
import {verify} from "./verify";

export namespace accounts {

Expand All @@ -16,15 +17,7 @@ export namespace accounts {
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"
});
}
verify.requiredParams(params, ["type"]);

const connectedAccountId = (params as any).id || `acct_${generateId(16)}`;
const account: stripe.accounts.IAccount & any = { // The d.ts is out of date on this object and I don't want to bother.
Expand Down
18 changes: 8 additions & 10 deletions src/api/charges.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import * as stripe from "stripe";
import log = require("loglevel");
import {StripeError} from "./StripeError";
import {applyListOptions, generateId, stringifyMetadata} from "./utils";
import {
applyListOptions,
generateId,
stringifyMetadata
} from "./utils";
import {getEffectiveSourceTokenFromChain, isSourceTokenChain} from "./sourceTokenChains";
import {cards} from "./cards";
import {AccountData} from "./AccountData";
import {customers} from "./customers";
import {disputes} from "./disputes";
import {refunds} from "./refunds";
import {verify} from "./verify";

export namespace charges {

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

const validCurrencies = ["usd", "aed", "afn", "all", "amd", "ang", "aoa", "ars", "aud", "awg", "azn", "bam", "bbd", "bdt", "bgn", "bif", "bmd", "bnd", "bob", "brl", "bsd", "bwp", "bzd", "cad", "cdf", "chf", "clp", "cny", "cop", "crc", "cve", "czk", "djf", "dkk", "dop", "dzd", "egp", "etb", "eur", "fjd", "fkp", "gbp", "gel", "gip", "gmd", "gnf", "gtq", "gyd", "hkd", "hnl", "hrk", "htg", "huf", "idr", "ils", "inr", "isk", "jmd", "jpy", "kes", "kgs", "khr", "kmf", "krw", "kyd", "kzt", "lak", "lbp", "lkr", "lrd", "lsl", "mad", "mdl", "mga", "mkd", "mmk", "mnt", "mop", "mro", "mur", "mvr", "mwk", "mxn", "myr", "mzn", "nad", "ngn", "nio", "nok", "npr", "nzd", "pab", "pen", "pgk", "php", "pkr", "pln", "pyg", "qar", "ron", "rsd", "rub", "rwf", "sar", "sbd", "scr", "sek", "sgd", "shp", "sll", "sos", "srd", "std", "szl", "thb", "tjs", "top", "try", "ttd", "twd", "tzs", "uah", "ugx", "uyu", "uzs", "vnd", "vuv", "wst", "xaf", "xcd", "xof", "xpf", "yer", "zar", "zmw", "eek", "lvl", "svc", "vef"];

const minChargeAmount: { [code: string]: number } = {
usd: 50,
aud: 50,
Expand Down Expand Up @@ -46,6 +49,7 @@ export namespace charges {
log.debug("charges.create", accountId, params);

handlePrechargeSpecialTokens(params.source);
verify.requiredParams(params, ["amount", "currency"]);
if (params.amount < 1) {
throw new StripeError(400, {
code: "parameter_invalid_integer",
Expand All @@ -64,13 +68,7 @@ export namespace charges {
type: "invalid_request_error"
});
}
if (validCurrencies.indexOf(params.currency.toLowerCase()) === -1) {
throw new StripeError(400, {
message: `Invalid currency: ${params.currency.toLowerCase()}. Stripe currently supports these currencies: ${validCurrencies.join(", ")}`,
param: "currency",
type: "invalid_request_error"
});
}
verify.currency(params.currency.toLowerCase(), "currency");
if (minChargeAmount[params.currency.toLowerCase()] && +params.amount < minChargeAmount[params.currency.toLowerCase()]) {
throw new StripeError(400, {
code: "amount_too_small",
Expand Down
9 changes: 6 additions & 3 deletions src/api/customers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {StripeError} from "./StripeError";
import {applyListOptions, generateId, stringifyMetadata} from "./utils";
import {cards} from "./cards";
import {AccountData} from "./AccountData";
import {verify} from "./verify";

export namespace customers {

Expand Down Expand Up @@ -170,14 +171,16 @@ export namespace customers {
}

export function addSubscription(accountId: string, customerId: string, subscription: stripe.subscriptions.ISubscription): void {
const customer = retrieve(accountId, customerId, "customer")
customer.subscriptions.data.push(subscription)
customer.subscriptions.total_count++
const customer = retrieve(accountId, customerId, "customer");
customer.subscriptions.data.push(subscription);
customer.subscriptions.total_count++;
}

export function createCard(accountId: string, customerOrId: string | stripe.customers.ICustomer, params: stripe.customers.ICustomerSourceCreationOptions): stripe.cards.ICard {
log.debug("customers.createCard", accountId, customerOrId, params);

verify.requiredParams(params, ["source"]);

const customer = typeof customerOrId === "object" ? customerOrId : retrieve(accountId, customerOrId, "customer");
if (typeof params.source === "string") {
const card = cards.createFromSource(params.source);
Expand Down
98 changes: 98 additions & 0 deletions src/api/plans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as stripe from "stripe";
import log = require("loglevel");
import {AccountData} from "./AccountData";
import {applyListOptions, generateId, stringifyMetadata} from "./utils";
import {StripeError} from "./StripeError";
import {verify} from "./verify";
import {products} from "./products";

export namespace plans {

const accountPlans = new AccountData<stripe.plans.IPlan>();

export function create(accountId: string, params: stripe.plans.IPlanCreationOptions): stripe.plans.IPlan {
log.debug("plans.create", accountId, params);

verify.requiredParams(params, ["currency", "interval", "product"]);
verify.requiredValue(params, "billing_scheme", ["per_unit", "tiered", null, undefined]);
verify.requiredValue(params, "interval", ["day", "month", "week", "year"]);
verify.requiredValue(params, "usage_type", ["licensed", "metered", null, undefined]);
verify.currency(params.currency, "currency");

const planId = params.id || `plan_${generateId(14)}`;
if (accountPlans.contains(accountId, planId)) {
throw new StripeError(400, {
code: "resource_already_exists",
doc_url: "https://stripe.com/docs/error-codes/resource-already-exists",
message: "Plan already exists.",
type: "invalid_request_error"
});
}

let product: stripe.products.IProduct;
if (typeof params.product === "string") {
product = products.retrieve(accountId, params.product, "product");
if (product.type !== "service") {
throw new StripeError(400, {
message: `Plans may only be created with products of type \`service\`, but the supplied product (\`${product.id}\`) had type \`${product.type}\`.`,
param: "product",
type: "invalid_request_error"
});
}
} else {
product = products.create(accountId, {
...params.product,
type: "service"
});
}

const billingScheme = params.billing_scheme || "per_unit";
const usageType = params.usage_type || "licensed";
const plan: stripe.plans.IPlan = {
id: planId,
object: "plan",
active: params.hasOwnProperty("active") ? (params as any).active : true,
aggregate_usage: usageType === "metered" ? params.aggregate_usage || "sum" : null,
amount: billingScheme === "per_unit" ? +params.amount : null,
billing_scheme: billingScheme,
created: (Date.now() / 1000) | 0,
currency: params.currency,
interval: params.interval,
interval_count: params.interval_count || 1,
livemode: false,
metadata: stringifyMetadata(params.metadata),
nickname: params.nickname || null,
product: product.id,
tiers: params.tiers || null,
tiers_mode: params.tiers_mode || null,
transform_usage: params.transform_usage || null,
trial_period_days: params.trial_period_days || null,
usage_type: usageType
};
accountPlans.put(accountId, plan);
return plan;
}

export function retrieve(accountId: string, planId: string, paramName: string): stripe.plans.IPlan {
log.debug("plans.retrieve", accountId, planId);

const plan = accountPlans.get(accountId, planId);
if (!plan) {
throw new StripeError(404, {
code: "resource_missing",
doc_url: "https://stripe.com/docs/error-codes/resource-missing",
message: `No such plan: ${planId}`,
param: paramName,
type: "invalid_request_error"
});
}
return plan;
}

export function list(accountId: string, params: stripe.IListOptions): stripe.IList<stripe.plans.IPlan> {
log.debug("plans.list", accountId, params);

let data = accountPlans.getAll(accountId);
return applyListOptions(data, params, (id, paramName) => retrieve(accountId, id, paramName));
}
}
Loading

0 comments on commit 48c68a3

Please sign in to comment.