QR Send

スマホとPCのChrome間で、URLやテキストをQRコード1回のペアリングで送り合える自作Chrome拡張。アカウント不要・無料。

使ってみる →

Playwrightのテストにパスワードをベタ書きしない:.env・storageState・CI secretsの使い分け

Selenium・自動化

page.fill(‘#password’, ‘hunter2’) と書いてしまった話

Playwrightでログインが必要なアプリのテストを書き始めると、最初にぶつかる壁がログイン処理そのものです。とりあえず動かしたくて、こう書いてしまったことはないでしょうか。

await page.fill('#username', 'testuser');
await page.fill('#password', 'hunter2');
await page.click('button[type="submit"]');

私も最初はこうでした。ローカルで動けばいいと思ってベタ書きし、そのままgit addしてしまう。この記事を読んでいる方の中にも、心当たりがある人は少なくないと思います。

動くには動きます。ですが、このコードはリポジトリに残り続けます。テスト用アカウントとはいえパスワードが平文でGitHubに上がるのは、あまり褒められた状態ではありません。しかもこの後、テストを実行するたびに毎回ログイン処理が走るので、テストがどんどん遅くなっていきます。

今回は、この「ベタ書き問題」を①.envとCI secretsでの認証情報の逃がし方 ②storageStateでログインを使い回す方法 ③そのstorageState.jsonの扱い、の3段階で整理します。

まず認証情報をコードから追い出す:.envとCI secrets

最初にやるべきことはシンプルです。パスワードをコードに書かず、環境変数から読むようにします。ローカルではdotenvパッケージを使うのが定番です。

npm install dotenv

playwright.config.tsの先頭でdotenvを読み込み、.envファイルの中身をprocess.envに展開します。

import { defineConfig } from '@playwright/test';
import dotenv from 'dotenv';
import path from 'path';

dotenv.config({ path: path.resolve(__dirname, '.env') });

.envにはこう書きます。

TEST_USER_EMAIL=test@example.com
TEST_USER_PASSWORD=hunter2

テストコード側はprocess.envから読むだけです。

await page.fill('#username', process.env.TEST_USER_EMAIL!);
await page.fill('#password', process.env.TEST_USER_PASSWORD!);

そしてこの.env必ず.gitignoreに入れます。これを忘れると本末転倒なので、最初にやってください。代わりに、キーだけ書いて値を空にした.env.exampleをコミットしておくと、他の人がセットアップするときに迷いません。

ではCI(GitHub Actions)ではどうするか。CIには.envファイル自体を持ち込みません。リポジトリのSecretsに登録した値を、ワークフローYAMLの中で環境変数として注入します。

- name: Run Playwright tests
  run: npx playwright test
  env:
    TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
    TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}

ローカルは.envファイル、CIは実行環境が用意してくれる環境変数。ここが素朴に混同されがちなところで、「CIでも.envファイルを置かないといけないのでは」と思い込んでしまう人がいます。CIのSecretsはすでに環境変数として渡ってくるので、ファイルは要りません。process.env.TEST_USER_PASSWORDという読み方だけ揃えておけば、ローカルもCIも同じコードで動きます。

ここまでで、少なくとも「パスワードがコードに書いてある」状態は解消できました。ただ、これだけだとまだ毎回ログイン処理を走らせることになります。

毎テストでログインし直さない:storageState

ログインフォームへの入力とボタンクリックは、地味に時間がかかります。テストが10本あれば10回ログインすることになり、これがテスト全体を遅くする原因のひとつになります。

Playwrightにはこれを解決するstorageStateという仕組みがあります。一度ログインした状態のブラウザコンテキストから、Cookie・localStorage・IndexedDBなどをJSONファイルに書き出し、以降のテストではそのファイルを読み込むだけでログイン済み状態を再現できます。

まず、ログインだけを行うセットアップ用のテストファイルを用意します。

// auth.setup.ts
import { test as setup } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('#username', process.env.TEST_USER_EMAIL!);
  await page.fill('#password', process.env.TEST_USER_PASSWORD!);
  await page.click('button[type="submit"]');
  await page.waitForURL('https://example.com/dashboard');

  await page.context().storageState({ path: authFile });
});

playwright.config.ts側で、このセットアップをproject dependenciesとして先に走らせ、他のプロジェクトは保存済みのstorageStateを使うように設定します。

export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /auth\.setup\.ts/ },
    {
      name: 'chromium',
      use: { storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
    },
  ],
});

これで、ログインするテストは`setup`プロジェクトだけになります。他のテストは最初からログイン済みの状態でスタートできるので、フォーム入力とクリックの分だけ確実に速くなります。実行してみると、ログイン処理が最初の1回しか走っていないのが分かるはずです。速くなったのを見ると、ちょっと感動します。

複数の権限(管理者・一般ユーザーなど)を扱う場合は、user.jsonadmin.jsonのように役割ごとにファイルを分けて、プロジェクトごとに読み込むstorageStateを切り替えれば対応できます。

そのstorageState.jsonは、鍵そのものです

ここが今回いちばん言いたいところです。storageStateの便利さに気を取られていると、うっかり見落とすことがあります。

playwright/.auth/user.jsonの中身を実際に開いてみてください。ログイン済みセッションのCookieが、有効期限つきでそのまま入っています。アプリによってはlocalStorageにアクセストークンが入っていることもあります。

つまりこのファイルは、パスワードそのものではないものの、そのセッションを乗っ取れるだけの情報が入った「鍵」です。これがリポジトリにコミットされてしまうと、誰でもそのCookieを使って該当アカウントとしてログインできてしまいます。テスト用アカウントだから大丈夫、とは言い切れません。テスト用アカウントの権限が管理者相当になっているケースは珍しくないからです。

対策はシンプルですが、必ずやってください。

# .gitignore
playwright/.auth/

Playwright公式のドキュメントでも、認証ファイルには「あなたになりすましたり、テストアカウントを悪用したりするために使える機密なCookieやヘッダーが含まれる可能性がある」として、プライベートリポジトリであってもチェックインは強く非推奨としています。公開・非公開を問わず、コミットしない前提で運用するのが正解です。

もう一つ正直に書いておきたいのが、「もう`.gitignore`に入れたから安心」で終わってはいけないということです。.gitignoreが効くのは、これから先の話でしかありません。すでに一度コミットしてしまったファイルは、履歴の中にそのまま残り続けます。過去にコミットしていないか、心当たりがあるリポジトリでは一度確認しておいたほうがいいです。

確認にはgitleaksのようなツールが使えます。git履歴全体をスキャンして、パスワードらしき文字列やトークンらしきパターンが含まれるコミットを洗い出してくれます。GitHub Actionsに組み込めば、プルリクエストのたびに新しい漏えいがないかを自動でチェックできますし、GitHubのSecret scanning機能も同様の役割を果たしてくれます。もし過去のコミットから見つかった場合は、該当のCookie・トークンを発行し直す(無効化する)のが一番確実です。履歴から消すだけでは、すでに誰かが見てしまった可能性まではゼロにできません。

AIに任せる時ほど、この整理が効いてくる

ここまでの話は、人間が手でPlaywrightのテストを書く場合でも当然大事なのですが、AIにテストを書かせたり、AIエージェントにブラウザ操作を任せたりする場面では、もう一段気をつけたほうがいいと思っています。

以前Playwright MCPの入門記事を書きましたが、AIにブラウザを触らせる運用が広がるほど、「ログイン済みのブラウザプロファイルをそのままAIに使わせる」場面も増えていきます。プロンプトインジェクションのリスクを整理した記事でも書いた通り、AIが読み込むページの中身がそのまま指示に化けうる以上、そのAIが認証済みセッションの鍵まで握っている状態は、被害が起きたときの影響範囲をそのまま広げてしまいます。今回の.env・storageStateの整理は、地味ですがAIに安全に任せるための土台でもあります。

もうひとつ実務的な注意点として、trace.zipや動画などの実行証跡にも認証情報がそのまま写り込むことがあります。trace CLIと動画証跡の記事でも触れましたが、CIのアーティファクトとして公開リポジトリに残してしまうと、それはそれで別の漏えい経路になります。保存先の権限やアーティファクトの保持期間まで含めて見直しておくと安心です。

まとめ:ベタ書き→.env/secrets→storageState→鍵の管理、の順で

というわけで、Playwrightのログインテストにまつわる認証情報の扱いを整理しました。

  • パスワードはコードに書かず、ローカルは.env(dotenv)、CIはSecretsから環境変数として渡す
  • 毎テストでログインし直さず、storageStateと project dependencies でログイン済み状態を使い回す
  • そのstorageState.json自体がCookieの入った鍵なので、.gitignoreに入れつつ、すでにコミットしていないかgitleaks等で履歴も確認する

正直なところ、これを全部やってもリスクはゼロにはなりません。テスト用アカウントの権限設計や、CIのアーティファクトの扱いまで含めて、地味な見直しを積み重ねるしかないというのが今の私の理解です。ただ、少なくとも「パスワードがそのままリポジトリに残っている」状態からは今日抜け出せます。手元のテストコードにpage.fill('#password', '...')とベタ書きされた箇所が残っていないか、この機会に一度検索してみてください。

関連記事も合わせてどうぞ。

コメント

タイトルとURLをコピーしました