Testing

Testing an app's billing without Stripe, and the plugin's own checks

# Testing

An app's billing can be tested without Stripe: the HTTP layer has a fake,
the webhook takes a header the test signs itself, and the events can be
recorded instead of delivered.

## Setting up

```pascal
uses
  Askr.Testing, Askr.Http.Router, Askr.Core.Clock, Askr.Urd.Model,
  Askr.Norn.Migration, Askr.Events,
  Askr.Plugin.Stripe, Askr.Plugin.Stripe.Client,
  Askr.Plugin.Stripe.Billing, Askr.Plugin.Stripe.Signature;

var
  Fake: TFakeStripeHttp;

procedure Fresh;
var
  M: TMigrator;
  C: TStripeClient;
begin
  UseTestDatabase;                   { sqlite::memory:, the plugin's tables }
  M := TMigrator.Create(CurrentDb);
  try
    M.Up;
  finally
    M.Free;
  end;

  Fake.Free;
  Fake := TFakeStripeHttp.Create;
  C := TStripeClient.Create('sk_test_suite');
  C.UseHttp(Fake, False);            { the test keeps the fake }
  SetStripe(C);                      { the client every call goes through }
  SetStripeWebhookSecret('whsec_suite');
end;
```

## What the app sends

`Fake.Queue(Json, Status)` sets the reply to the next request, and
`Fake.Last` is what was sent: `Method`, `Url`, `Form`, `IdempotencyKey`.

```pascal
procedure TestCheckout;
var
  Url: string;
begin
  Fresh;
  Fake.Queue('{"id":"cus_1"}');                      { the customer }
  Fake.Queue('{"id":"cs_1","url":"https://checkout.stripe.com/c/pay/cs_1"}');
  Url := CheckoutUrl('7', 'ada@example.com',
    SubscriptionCheckout('price_pro', 'https://x/ok', 'https://x/no'));
  AssertEqual(Url, 'https://checkout.stripe.com/c/pay/cs_1', 'the page');
  AssertContains(Fake.Last.Form, 'mode=subscription', 'a subscription');
end;
```

A request with no reply queued raises, so a test cannot pass by sending
more than it meant to.

## A webhook of your own

Post an event through the real route, signed with the test's secret, and
the plugin does exactly what it does with Stripe's:

```pascal
procedure TestWelcome;
var
  R: TRouter;
  C: TTestClient;
  Body: string;
begin
  Fresh;
  FakeEvents([]);                    { record, don't deliver }
  Fake.Queue('{"id":"cus_A"}');
  EnsureStripeCustomer('7', 'ada@example.com');   { user 7 is cus_A }
  Body := '{"id":"evt_1","object":"event","created":' + IntToStr(UnixNow) +
    ',"type":"customer.subscription.created","data":{"object":' +
    '{"id":"sub_1","object":"subscription","customer":"cus_A",' +
    '"status":"active","items":{"data":[{"id":"si_1",' +
    '"price":{"id":"price_pro"},"quantity":1}]}}}}';

  R := TRouter.Create;
  R.Post('/stripe/webhook', StripeWebhook);   { the plugin's own handler }
  C := TTestClient.Create(R);
  try
    AssertStatus(C.WithHeader('Stripe-Signature',
        StripeSignatureHeader(Body, 'whsec_suite', UnixNow))
      .Post('/stripe/webhook', Body), 200, 'taken');
  finally
    C.Free;
    R.Free;
  end;
  AssertTrue(Subscribed('7', 'price_pro'), 'subscribed');
  AssertEqual(EventsDispatched(TStripeSubscriptionCreated), 1, 'told once');
end;
```

`FakeEvents([])` records every event instead of running its listeners;
`EventsDispatched` counts them and `DispatchedEventJson` gives one's
fields. Leave it out, and your real listeners run — which is how to test
them.

## Against stripe-mock

[stripe-mock](https://github.com/stripe/stripe-mock) is Stripe's own
server built from its OpenAPI spec. It answers every endpoint with a
fixture and refuses a top-level parameter Stripe does not have:

```sh
docker run --rm -p 12111:12111 stripe/stripe-mock
```

```pascal
SetStripe(TStripeClient.Create('sk_test_mock', 'http://localhost:12111'));
```

In a test the plugin's `Configure` does not run, so the client is made by
hand; in a running app, `STRIPE_BASE_URL` does the same.

It does not check keys nested inside a parameter, like
`line_items[0][price]`, and it keeps nothing: a customer made there is not
there when you ask for it. It proves the shape of a request, not what
Stripe would do with it.

## What the plugin itself is held to

The plugin's repository runs four checks, each for what the others
cannot see:

| | |
|---|---|
| `./check` | The suite: the signature check held to stripe-python's verdict on thirty headers, every request held to stripe-mock, the headers read off a raw socket, and the webhook's order, duplicates and rollback |
| `./check --checks` | The same with range, overflow and I/O checks |
| `./check --app` | A new `askr new --auth` app: the plugin added from a tag, built, migrated, and a signed webhook over HTTP through sessions, the rate limit and CSRF |
| `STRIPE_SECRET=sk_test_... ./check --testmode` | A real test-mode account, with `stripe listen` forwarding its own events |

The last is run by hand, with a test key: it makes and deletes customers,
a product and subscriptions in the account. It has been run, and it found
a bug the other three could not — the customer's idempotency key, in
[customers](customers.md). What none of them prove is live mode, a real
card, or a Checkout page completed by a person.