Stripe Webhook用のAPIをStripe CLIでテストする方法

StripeではWebhookを利用したイベント駆動のシステム開発がとても重要です。

しかし、Webhookの開発・動作確認をするには、Stripeが発火するWebhookイベントのデータを再現する必要があります。

そこで今回は、Stripe CLIを利用したWebhook　APIのテスト方法について紹介します。

# Stripe CLIを利用して、Webhookをローカルテストする

まずは開発中のAPIをテストする方法を紹介します。

Stripe CLIには、WebhookイベントをローカルのAPIにforwardするコマンドが用意されています。

なお、Stripe CLIの設定方法や基本的な使い方については、以下の電子書籍や公式ドキュメントをご覧ください。

https://stripe.com/docs/webhooks

https://zenn.dev/hideokamoto/books/e961b4bad92429

## Stripe CLIでWebhookイベントをローカルAPIに中継する

ローカルAPIにイベントを中継するには、以下のコマンドを実行します。

```bash
$ stripe listen --forward-to localhost:4242/webhook
⡿ Getting ready... > Ready! You are using Stripe API Version [2020-08-27]. Your webhook signing secret is whsec_xxxxx (^C to quit)
```

`--forward-to`のURLは、起動中のローカルWebhook APIのURL・パスに変更してください。

この状態で、Stripe DashboardやAPIの操作を行うと、以下のように受信したイベントの種類やIDが確認できます。

```bash
2022-01-25 10:20:07   --> charge.succeeded [evt_xxxx]
2022-01-25 10:20:07  <--  [200] POST http://localhost:4242/webhook [evt_xxx]
2022-01-25 10:20:07   --> payment_intent.succeeded [evt_xxxx]
2022-01-25 10:20:07  <--  [200] POST http://localhost:4242/webhook [evt_xxx]
2022-01-25 10:20:07   --> payment_intent.created [evt_xxxx]
2022-01-25 10:20:07  <--  [200] POST http://localhost:4242/webhook [evt_xxxx]
```

##　Stripe CLIで任意のWebhookイベントを実行する

特定のWebhookイベントをテストしたい場合、`stripe trigger`コマンドを使ってイベントを手動発火できます。

```bash
% stripe trigger payment_intent.created
⡿ Checking for new versions... A newer version of the Stripe CLI is available, please update to: v1.7.9
Setting up fixture for: payment_intent
Running fixture for: payment_intent
Trigger succeeded! Check dashboard for event details.
``` 

なお、イベントの種類によっては、そのイベントを発火するために必要なAPIコールによる、別イベントの発火が発生します。
そのため、API側で全てのリクエストをログ出力するテスト実装をしている場合などでは、無関係なイベントのログも発生するケースがあります。

```bash
% stripe trigger customer.subscription.updated
⡿ Checking for new versions... A newer version of the Stripe CLI is available, please update to: v1.7.9
Setting up fixture for: customer
Running fixture for: customer
Setting up fixture for: plan
Running fixture for: plan
Setting up fixture for: subscription
Running fixture for: subscription
Setting up fixture for: subscription_updated
Running fixture for: subscription_updated
Trigger succeeded! Check dashboard for event details.
```

この例では、customer / plan / subscriptionがそれぞれ作成されています。
そのため、イベントログが以下のように長大になります。


```bash
2022-01-25 10:20:07   --> payment_intent.created [evt_3KLdhaKlXtpaTqYx0lqofB15]
2022-01-25 10:20:07  <--  [200] POST http://localhost:4242/webhook [evt_3KLdhaKlXtpaTqYx0lqofB15]
2022-01-25 10:24:25   --> payment_method.attached [evt_1KLdllKlXtpaTqYxqPWzKraN]
2022-01-25 10:24:25  <--  [200] POST http://localhost:4242/webhook [evt_1KLdllKlXtpaTqYxqPWzKraN]
2022-01-25 10:24:25   --> customer.source.created [evt_1KLdllKlXtpaTqYxxNnbV3VH]
2022-01-25 10:24:25  <--  [200] POST http://localhost:4242/webhook [evt_1KLdllKlXtpaTqYxxNnbV3VH]
2022-01-25 10:24:25   --> customer.created [evt_1KLdllKlXtpaTqYxxTI1CwQD]
2022-01-25 10:24:25  <--  [200] POST http://localhost:4242/webhook [evt_1KLdllKlXtpaTqYxxTI1CwQD]
2022-01-25 10:24:26   --> plan.created [evt_1KLdlmKlXtpaTqYx30hwQmXI]
2022-01-25 10:24:26  <--  [200] POST http://localhost:4242/webhook [evt_1KLdlmKlXtpaTqYx30hwQmXI]
2022-01-25 10:24:26   --> price.created [evt_1KLdlmKlXtpaTqYxdH8NbRJV]
2022-01-25 10:24:26  <--  [200] POST http://localhost:4242/webhook [evt_1KLdlmKlXtpaTqYxdH8NbRJV]
2022-01-25 10:24:29   --> charge.succeeded [evt_3KLdlnKlXtpaTqYx1meAb8GZ]
2022-01-25 10:24:29  <--  [200] POST http://localhost:4242/webhook [evt_3KLdlnKlXtpaTqYx1meAb8GZ]
2022-01-25 10:24:29   --> customer.updated [evt_1KLdlpKlXtpaTqYxskyqLAU4]
2022-01-25 10:24:29  <--  [200] POST http://localhost:4242/webhook [evt_1KLdlpKlXtpaTqYxskyqLAU4]
2022-01-25 10:24:29   --> invoice.finalized [evt_1KLdlpKlXtpaTqYxN12BGUGF]
2022-01-25 10:24:29  <--  [200] POST http://localhost:4242/webhook [evt_1KLdlpKlXtpaTqYxN12BGUGF]
2022-01-25 10:24:29   --> invoice.paid [evt_1KLdlpKlXtpaTqYxS7HS3m46]
2022-01-25 10:24:29  <--  [200] POST http://localhost:4242/webhook [evt_1KLdlpKlXtpaTqYxS7HS3m46]
2022-01-25 10:24:29   --> invoice.payment_succeeded [evt_1KLdlpKlXtpaTqYxgyGKRS4V]
2022-01-25 10:24:29  <--  [200] POST http://localhost:4242/webhook [evt_1KLdlpKlXtpaTqYxgyGKRS4V]
2022-01-25 10:24:29   --> customer.subscription.created [evt_1KLdlpKlXtpaTqYx92HEUya5]
2022-01-25 10:24:29  <--  [200] POST http://localhost:4242/webhook [evt_1KLdlpKlXtpaTqYx92HEUya5]
2022-01-25 10:24:29   --> payment_intent.succeeded [evt_3KLdlnKlXtpaTqYx1WyUcYIV]
2022-01-25 10:24:29  <--  [200] POST http://localhost:4242/webhook [evt_3KLdlnKlXtpaTqYx1WyUcYIV]
2022-01-25 10:24:29   --> payment_intent.created [evt_3KLdlnKlXtpaTqYx1aLVsIJK]
2022-01-25 10:24:29  <--  [200] POST http://localhost:4242/webhook [evt_3KLdlnKlXtpaTqYx1aLVsIJK]
2022-01-25 10:24:30   --> invoice.created [evt_1KLdlpKlXtpaTqYxmmhkmqDk]
2022-01-25 10:24:30  <--  [200] POST http://localhost:4242/webhook [evt_1KLdlpKlXtpaTqYxmmhkmqDk]
2022-01-25 10:24:30   --> customer.subscription.updated [evt_1KLdlqKlXtpaTqYx2s2YvqiK]
2022-01-25 10:24:30  <--  [200] POST http://localhost:4242/webhook [evt_1KLdlqKlXtpaTqYx2s2YvqiK]
```

## より実践的なデータでテストする

バグ修正や特定のユースケースへの対応など、`trigger`コマンドで生成されるイベントデータではテストが不十分なケースも存在します。

その場合、よりユースケースに近い形のテストデータを作成する必要があります。

### Stripe CLIで「任意のデータを作成」する

Stripe CLIを利用して、Stripe APIを実行することも可能です。

```bash
% stripe customers create --name FromCLI
{
  "id": "cus_xxxx",
  "object": "customer",
  "address": null,
  "balance": 0,
  "created": 1643074805,
  "currency": null,
  "default_source": null,
  "delinquent": false,
  "description": null,
  "discount": null,
  "email": null,
  "invoice_prefix": "2A573CEE",
  "invoice_settings": {
    "custom_fields": null,
    "default_payment_method": null,
    "footer": null
  },
  "livemode": false,
  "metadata": {
  },
  "name": "FromCLI",
  "next_invoice_sequence": 1,
  "phone": null,
  "preferred_locales": [

  ],
  "shipping": null,
  "tax_exempt": "none"
}
```

この方法でも、Webhookイベントは送信されます。

そのため、「特定のデータが作成された時の振る舞いを確認する」方法として、テスト用のStripe CLIコマンドリストを作成し、それを実行する手法も検討することができます。


### Stripe CLIで「過去のWebhookイベントデータを取得」し、結合テストに利用する

もう１つの対策として、「実際のデータをJSONで取得してしまう」方法を採用することもできます。

`stripe events resend　<event_id>`コマンドを実行すると、過去に実際に発生したイベントで送信されたデータをJSONで取得することができます。

```bash
$ stripe events resend evt_xxxxx
{
  "id": "evt_xxxxx",
  "object": "event",
  "api_version": "2020-08-27",
  "created": 1643073864,
  "data": {
    "object": {
      "id": "card_xxxxx",
      "object": "payment_method",
      "billing_details": {
        "address": {
          "city": null,
          "country": null,
          "line1": null,
          "line2": null,
          "postal_code": null,
          "state": null
        },
        "email": null,
        "name": null,
        "phone": null
      },
      "card": {
        "brand": "visa",
        "checks": {
          "address_line1_check": null,
          "address_postal_code_check": null,
          "cvc_check": null
        },
        "country": "US",
        "exp_month": 1,
        "exp_year": 2023,
        "fingerprint": "xxxxx",
        "funding": "credit",
        "generated_from": null,
        "last4": "4242",
        "networks": {
          "available": [
            "visa"
          ],
          "preferred": null
        },
        "three_d_secure_usage": {
          "supported": true
        },
        "wallet": null
      },
      "created": 1643073864,
      "customer": "cus_xxxx",
      "livemode": false,
      "metadata": {
      },
      "type": "card"
    }
  },
  "livemode": false,
  "pending_webhooks": 1,
  "request": {
    "id": "req_xxxx",
    "idempotency_key": "123-456-789"
  },
  "type": "payment_method.attached"
}
```

このデータをコピーし、Jestなどのテストライブラリで結合テストとして処理を実行する方法も、ローカルでのテスト方法としては有効です。

# デプロイしたAPIへのテストをStripe CLIで実行する

開発中のテストについては、Stripe CLIのforward機能とtriggerコマンドである程度対応できます。
ですが、運用中に「想定外のWebhookエラー」が発生するケースも少なくありません。
その場合は、「問題のあったリクエストをそのまま再現」することが、問題の確認や修正方法の検討に役立ちます。

その場合に有効な方法が、`stripe events resend`コマンドです。

## 再送信したいイベントのIDを取得する

再送信したいイベントのIDは、Dashboardまたはログから確認できます。

ログでは、`event.type`や`event.data.object`の内容を確認しつつ、`event.id`の値(`evt_xxx`)を記録しましょう。

Dashboardからは、[開発者 > イベント]からログとIDの確認ができます。

![スクリーンショット 0004-01-25 10.51.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/731969eb-e09a-c5a5-a992-2e53ee794f63.png)


## 再送信したいWebhookのIDを取得する

続いてDashboardの[開発者 > Webhook]で、イベントを再送信したいWebhookのID(`we_xxx`)を取得しましょう。

![スクリーンショット 0004-01-25 10.44.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/0eb4d51a-915b-69fe-b12d-46d0cc62ba8c.png)

## Stripe CLIでイベントを再実行する

あとはStripe CLIでイベントを再送信するだけです。
先ほど確認した２つのIDを利用して、コマンドを実行しましょう。

```bash
% stripe events resend evt_xxxxx --webhook-endpoint we_xxxx
```

これで指定したAPIに対して、同じイベントが再度送信されます。

# テストする際に注意が必要な点

最後に、Webhook APIを実装・テストする上で注意したい点を紹介します。

## Webhook署名シークレットの存在を忘れない

Stripe Webhookに使用するAPIには、署名シークレットの設定が必要です。

https://stripe.com/docs/webhooks/signatures

以下はExpress(Node.js)での実装サンプル抜粋です。

```js
  const stripe = require('stripe')('sk_test_xxxxx');
  const endpointSecret = 'whsec_...';
  try {
    event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
  }
  catch (err) {
    response.status(400).send(`Webhook Error: ${err.message}`);
  }
```

Stripe以外からAPIが実行された場合、この署名検証部分でエラーになる可能性があります。

Stripe CLIなどを利用してStripe経由でイベントを発火するか、署名検証以降の処理を別関数化し、そちらで結合テストを実行することをお勧めします。

## 「本物のイベント」も受信するケースがあります

Stripe CLIでWebhookイベントをローカルAPIに中継する場合、テストのために発火したイベント以外に、「アプリ・システムから送信される本物のイベント」も送られてくることに注意しましょう。

もちろん「本物」とはいえ、**テスト環境に本番のデータがくることはありません**。

ですが、ローカルのAPIは開発途中である可能性が高く、意図しないイベントを受信すると不具合が発生する可能性があります。

大きな変更を伴う作業中の場合、`--event`フラグなどで不必要なイベントを中継させないようにしましょう。

```bash
# 顧客作成・更新のイベントだけ中継する
% stripe listen --forward-to localhost:4242/webhook --events customer.created,customer.updated
```

## CIでのテストについて

もっとも手軽にテストする方法は、`stripe events resend　<event_id>`などを用いてイベントデータを取得し、そのデータをテストで利用する結合テストを作ることです。

この場合、StripeのAPIやWebhookイベントに依存せずテストを実行できますので、実行時間や冪等性などを考慮する必要がなくなります。

ただしもちろん、テストケースを追加するたびにJSONデータを取得・保存する手間が発生します。

そのほかのアイディアとしては、「Docker経由でStripe CLIを実行する方法」や「JestなどでStripe APIを利用し、APIを実際に実行する」、「AWSなどのクラウドサービスで、ログ・モニタリングツールがエラーを検知したらロールバックする、テストAPIスタックをデプロイする方法」などが考えられるかと思います。

今のところ、Stripeが公開しているOSS・ツールでCIサービス上でのWebhook APIをテストするためのものはありません。

そのため、「このような形でテストを試してみた！」や「こんな形でテストできるツールがあると嬉しい」などのコメントや記事を皆さんからいただけますと、とても助かります。

# 参考ドキュメント　・記事

https://stripe.com/docs/webhooks

https://stripe.com/docs/webhooks/quickstart

https://stripe.com/docs/webhooks/test

https://stripe.com/docs/webhooks/best-practices

https://stripe.com/docs/webhooks/go-live

https://qiita.com/hideokamoto/items/7c0e89778e245e35d0ff