FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

モックサーバ構築でCORS対応に手こずった話

TL;DR

  • フロントエンドのテストで、WireMockでモックを用意しただけではうまくいかなかった
  • 以下で解決した
    • WireMock側
      • 自己署名証明書でhttps化
      • CORSを受け付けるようにした
    • テスト用ブラウザ
      • 自己署名証明書をブロックしない設定にした

はじめに

こんにちは、ピクトリンク事業部開発部サーバサイド開発課、兼ホグワーツ在学中のkitajimaです。
弊社サービスピクトリンクのフロントエンドを対象にしたE2Eテストを実施するためにモックサーバを立てたのですが、その際に手こずった点とその対応を紹介します。

E2Eテストについて

ピクトリンクは、自動テスト用モジュール(以下、テストモジュール)を用いてE2Eテストを実施しています。
Selenideという自動テストフレームワークを利用してブラウザ操作の自動化、検証をし、画面遷移を伴うテストを実現しています。

Selenide.open("https://example.com");
Selenide.sleep(1000);
Selenide.$(".hoge").shouldHave(text("fuga"));

Selenideはブラウザ自動化のライブラリであるSeleniumのラッパであり、Seleniumとは違いテストに特化しています。ブラウザ操作のAPI群が隠蔽されているため、上記のように直感的な記述が可能です。

テスト対象のユースケース

現在ピクトリンクでは、中学生ピクトリンク無料サービスというサービスを提供しています。
フロントエンドでは、サービスに関するAPI(以下、サービス用API)が提供する、「サービス申込可否を判定するAPI」にリクエストを投げ、そのレスポンスを元に申込入力ページもしくはsorryページを出しわけています。

sequenceDiagram
participant chrome as ブラウザ
participant site as ピクトリンクサイト<br>(sp.pictlink.com)
participant B as サービス用API<br>(dev.campaign.example.com)


    chrome->>site: /サービス用ページ
    site->>chrome: html,js

    chrome-->>B: 申込可否問い合わせ
    Note left of B: アカウント情報
    
    B->>chrome: 申込可否判定
    chrome->>chrome: 申込入力画面 or sorry画面

今回はフロントエンドのテストのため、外部モジュールであるサービス用APIの振る舞いに依存したくありません。サービス用APIをモックサーバに置き換え、こんなテストが実現できると嬉しいです。

sequenceDiagram
participant chrome as テストブラウザ(Selenide)
participant site as ピクトリンクサイト(テスト)<br>(xxxx.pictlink.com)
participant B as サービス用API<br>(mock.campaign.example.com)


    chrome->>site: /サービス用ページ
    site->>chrome: html,js

    chrome-->>B: 申込可否問い合わせ
    Note left of B: アカウント情報
    
    B->>chrome: 申込可否判定
    chrome->>chrome: 期待するページであるか検証

モックを用意

WireMockを用いてモックサーバを立ててみました。ドキュメントに従ってdocker-compose.yamlを書いていきます。(XXXXは任意のポートです)

version: '3.7'

services:
  campaign-service-mock:
    image: 'wiremock/wiremock:2.32.0'
    ports:
      - "XXXX:443"
    volumes:
      - ./wiremock:/home/wiremock
    command: >
      --verbose
      --local-response-templating
      --https-port 443

WireMockは、デフォルトではhttpでのみリクエストを受け付けます。
しかしピクトリンクサイト自体がhttpsであるため、その中にhttpのコンテンツが存在する場合Mixed contentという状態になり、このままではアクセスがブロックされてしまいます。
これを避けるため、WireMockの起動オプションを追加し、https対応しています。

--https-port 443

テスト目的なので、今回はデフォルトで用意される自己署名証明書で良しとしました。

続いて、公式ドキュメントを参照に、JSON形式でモック対象のリクエストを作成します。

{
  "request": {
    "method": "GET",
    "urlPath": "/available",
    "headers": {
      "Authorization": {
        "matches": "Bearer .+"
      }
    }
  },
  "response": {
    "jsonBody": {
      "available": true
    }
  }
}

※以下、登場する*.example.comはダミーです。

これで、https://campaign.mock.example.com:XXXX/availableにBearerトークン付きでGETリクエストを投げると、期待したレスポンスが返るようになりました。

$ curl -H 'Authorization:Bearer hoge' https://campaign.mock.example.com:XXXX/available

{"available": true}

それでは自動テストに組み込んでみましょう。フロントエンドの設定ファイルにて、サービス用APIのURLをモックのものに置き換えました。

- campaignUrl: https://campaign.develop.example.com
+ campaignUrl: https://campaign.mock.example.com:XXXX

そして自動テストを実行すると・・・

アクセスが弾かれてしまいました。

原因: リクエストがCORSに該当していた

CORS(Cross-Origin Resource Sharing)によりブロックされていました。以下、ブラウザのエラーです。

Access to XMLHttpRequest at 'https://campaign.mock.example.com:XXXX/available' from origin 'https://sp.pictlink.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

サービス用APIはピクトリンクサイトとは別のホスト、つまり異なるオリジンです。 (なんならホストだけでなくポートも異なりますが)

ピクトリンクサイト: https://sp.pictlink.com
サービス用APIのモック: https://campaign.mock.example.com:XXXX

そのため、ピクトリンクサイトからサービス用APIのモックへのリクエストは、CORSに相当します。
CORSでは、事前確認用にpreflightリクエストと呼ばれるOPTIONSリクエストをブラウザが自動で発行します。

sequenceDiagram
participant chrome as ウェブ文書 from sp.pictlink.com
participant B as campaign.mock.example.com
    chrome->>B: OPTIONS /available
    B->>chrome: 許可情報

リクエストでは、Access-Control-Request-Methodで送りたいHTTPメソッド、Access-Control-Request-Headersで送りたいヘッダを指定します。

Access-Control-Request-Method: GET
Access-Control-Request-Headers: Authorization

レスポンスでは、Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headersといったヘッダで、それぞれ許可するオリジン、HTTPメソッド、ヘッダを返します。

また、Access-Control-Max-Ageで、ブラウザがpreflightの結果をキャッシュしてもよい時間を返します。

Access-Control-Allow-Origin: https://sp.pictlink.com
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 1800

なお、リクエストが特定のケース(単純リクエスト)に該当する場合はpreflightは発行されません。

つまり、サービス用APIのモックを実現するには、モックがCORSに対応する必要があるわけです。

やったこと① WireMock側の設定

CORSオプションを設定する

WireMockはデフォルトではpreflightリクエストに対応してくれません。 起動オプションに--enable-stub-corsを追加して解消します。 これにより、モックされた任意のマッピングに対してpreflightリクエストのマッピングも用意してくれるようになります。

以上で修正したdocker-compose.yamlがこちらです。

version: '3.7'

services:
  campaign-service-mock:
    image: 'wiremock/wiremock:2.32.0'
    ports:
      - "XXXX:443"
    volumes:
      - ./wiremock:/home/wiremock
    command: >
      --verbose
      --local-response-templating
      --enable-stub-cors
      --https-port 443

やったこと② Selenide側の設定

モック側をhttps対応したものの、ブラウザはデフォルトでは自己署名証明書を不正なものとしてエラーを返します。
WebDriverの acceptInsecureCertsという設定をtrueにすることでこれを回避してみます(自動テスト環境でのみ設定すべき内容であり、実際のブラウザでは推奨されるものではありません)。

この辺りは公式ドキュメントにお任せしてしまいますが、ChromeDriverの初期化のタイミングでオプションを以下のように渡せます。

ChromeOptions options = new ChromeOptions();
options.setAcceptInsecureCerts(true);
ChromeDriver chromeDriver = new ChromeDriver(options);

いざ実行

フロントエンドからモックサーバに対してアクセスができました。preflightリクエスト、実際のリクエストが並んでいます。

preflightリクエスト 実際のリクエスト

最後に

普段使っているwebフレームワーク(Spring Boot)はCORSに対してよしなに対応してくれているらしく、そのありがたさを感じた事象でした。参考になれば幸いです。