探索的テストが苦手なので AI に任せてみた — 1 巡目で 107 件、そのうち 1 件は緊急リリース
自分で作ったサービスは、正常系でしか触れない。探索的テストが苦手な個人開発者が、Claude Code に Playwright で全サービスを触らせてみた。候補は 107 件。人と 1 件ずつ仕分けて起票し、週次の運用に落とした。その中に、Amazon SNS の受け口が「署名は正しいのに、送り主が誰でもよかった」という本物の穴が混じっていた話。
自分で作ったものは、正常系でしか触れない
自分は個人で 10 個ほどの Web サービスを運用している。株価のアラート、動画の切り抜き、グループの共有リスト、管理画面など、どれも自分が使いたくて作ったものだ。
テストは E2E を書いている。CI でも回している。ただ、探索的テスト、つまり仕様書の外側を手で触って、おかしなところを探す作業は、正直に言って苦手だ。
理由ははっきりしている。自分で作ったものだから、作った通りの使い方しか思いつかない。入力欄には常識的な値を入れるし、ボタンは想定した順番で押す。ハッピーパスを外れる発想が、作り手の頭からは出てこない。
もう一つは経験値だ。自分がエンジニアとして触ってきたサービス、作ってきたサービスの数はそれほど多くない。「こういう画面ならここが壊れやすい」という勘が、そもそも足りていない。
結果として、これまでの探索的テストは自分の日常使いがすべてだった。自分のサービスは自分が使いたい機能ばかりなので、毎日使っていれば一応は触ることになる。それが探索的テストでもあり、回帰テストでもある、という状態だった。あとは E2E 頼み。
AI ならブラウザを触れる、と知っていた
Claude Code が Playwright でブラウザを操作できることは、知識としては知っていた。だったら、自分の苦手なところをそのまま任せられるのではないか。期待値は高めで、自分よりうまくやってくれるはずだと思っていた。
最初は「モンキーテストみたいなもの」と言って話を始めた。ただ、やりたいのはランダムに連打することではなく、触りながら「ここは怪しい」と当たりを付けて掘っていくことだ。話しているうちに、これは探索的テストと呼ぶほうが近いと言い直した。
方針として決めたことは 3 つある。
- 結果はブレていい。 毎回同じものが見つかる必要はない。毎回確実に見つかるものは、E2E に組み込めばいい
- いきなり運用にしない。 まず 1 回、全サービスで実験してみる
- コストはかかってもいい。 頻度は週 1 回で十分
認証の壁と、AWS の壁
全サービスを手元で起動して触らせる。言うのは簡単だが、2 つの壁があった。
1 つ目は認証だ。ログインが必要な機能が多い。ここは、E2E のために用意していたテスト用の仕組みがそのまま使えた。テスト用の設定で起動すると認証を飛ばせて、ロールもリクエストヘッダで差し替えられる。E2E のために作った足場が、探索にも効いた。
2 つ目は AWS だ。データベースにインメモリの実装を持っているサービスはそれで動く。しかし動画変換や切り抜きのように、Amazon S3 や AWS Batch を直接呼ぶサービスは、そのままでは画面が進まない。ここは AI に、S3 や DynamoDB の API を真似る偽の AWS サーバーを手元に書かせて、SDK の向き先を差し替えて動かした。
この AWS まわりで、一度回り道をしている。AI (Claude) が「実際の AWS に向けると、本物のデータに触れてしまう」と言い出した。こちらは、AI に渡している権限が読み取り中心に絞ってあることを知っている。「なんでインフラの定義を見ないの? そこを見たら何を付与しているかわかるはず」と返したら、AI は定義を読んで前提を訂正した。AI に探索させると、AI 自身の「思い込み」も一緒に出てくる。根拠をコードに当たらせれば正せる、というのは覚えておきたい。
サービスごとに探索担当を 1 体ずつ立てて、並列に走らせた。1 サービスあたり 10〜20 分ほど。報告はそのまま起票せず、まず候補に起こす。
候補は 107 件
出てきた候補は、10 サービスで 107 件。
初回なので、ある程度の数は覚悟していた。それでも想定以上だった。
ただ、細々したものばかりというわけでもない。入力値の検証漏れ、不正な JSON を送ると 500 が返る API、画面幅によってはみ出す要素など、言われてみれば確かにおかしいものが並んでいた。自分で気づいていなかったものが多かった。
前から「なんとなく気になっていた」ものも拾われていた。小さすぎて押しにくいチェックボックスや、サービスによって微妙に違うヘッダーなどだ。気になっていたが起票するほどでもないと放っていたものが、まとめて言語化されて出てきた。これは素直によかったと思う。
1 件ずつ、人が仕分ける
107 件をそのまま Issue にはしない。AI に候補を 1 件ずつ画面にまとめてもらい、現象・再現手順・原因の当たりを見ながら、起票するかどうかを決めていった。
途中で方針を変えている。全件を同じ熱量で見ていたら終わらない。リファクタの提案や、テストやドキュメントの改善案まで混ざっていたので、今回はバグと、ユーザーに影響があるものに限ると決めた。それ以外は見送り、見送ったものは記録もしない。置いておく場所がないし、本当に大事なら次の探索でまた見つかる。結果がブレていいと決めたのは、こういうところで効いてくる。
AI の報告を鵜呑みにしないことも大事だった。ある候補は「大きなファイルのアップロードが失敗するはず」という指摘だった。その機能は直近で動作確認したばかりだったので、開発環境のログを見てもらったら、実際には問題なくアップロードが完了していた。候補はあくまで仮説で、確かめるのは仕分ける側の仕事だ。
最終的に、細かいものはサービスごとに束ね、同じ原因で複数のサービスにまたがるものは横断の 1 本にまとめて、49 件を起票した。
そして 1 件だけ、Issue にせず、すぐに直したものがある。
署名は正しい。でも、送り主は誰でもよかった
管理画面には、自分の監視基盤から通知を受け取る受け口がある。CloudWatch のアラームが Amazon SNS のトピックに発行され、SNS が HTTPS でこの受け口を呼ぶ。受け口は届いたメッセージを Web Push に変えて、管理者の端末に通知を飛ばす。
受け口は、届いたメッセージの SNS 署名を検証していた。SNS のメッセージには AWS の署名が付いていて、証明書で検証すれば、そのメッセージが本当に SNS から送られたものかがわかる。偽物のリクエストは弾ける。ここまでは教科書通りだ。
探索で指摘されたのは、その先だった。SNS の署名は、どの AWS アカウントのトピックから送られても正規のものになる。 署名が証明するのは「SNS が送った」ことだけで、「自分のトピックから送られた」ことではない。
つまり、こういうことが起こりうる。
- 誰かが自分の AWS アカウントで SNS トピックを作り、この受け口を HTTPS の購読先として登録する
- SNS は購読確認のメッセージを受け口に送る。受け口は署名を検証し、正しいので購読を確定してしまう
- 以後、そのトピックに発行されたメッセージは、署名が正しいまま受け口に届き、管理者の端末に通知として表示される
署名検証は通っているのに、送り主を誰も確かめていなかった。
候補として見たとき、最初に湧いたのは焦りだった。自分の考慮漏れで、何か大きなミスをしていたのではないか、と。
落ち着いて考えると、影響を受けるのは管理者、つまり自分だけだ。データが漏れるわけでもない。そこまでクリティカルではなかったかもしれない。それでも、実害が起こりうるものを残し続けるのは好ましくない。そう判断して、本番の版から直接ブランチを切る緊急リリースに踏み込んだ。リポジトリは公開しているので、直す前に手口を Issue に書くこともしなかった。
どうすべきだったか
直し方はシンプルだ。SNS のメッセージには、送信元のトピックを示す TopicArn が含まれている。受け付けるトピックの ARN を決めておき、それ以外は拒否する。
// 署名が正しくても、送信元のトピックは任意。副作用の前に照合する
if (message.TopicArn !== allowedTopicArn) {
return forbidden();
}
if (message.Type === 'SubscriptionConfirmation') {
await fetch(message.SubscribeURL); // 購読の確定はここで初めて行う
}
ポイントは、照合を購読確認より前に置くことだ。通知の段階で弾くだけでは足りない。購読確認の段階で拒否すれば、そもそも他人のトピックの購読が成立しない。
整理すると、SNS の HTTPS 受け口が確かめるべきことは 2 つある。
- 署名: メッセージが本当に SNS から送られたか
- トピック: それが自分の受け付けるトピックか
前者だけでは、後者は何も保証されない。HTTPS で SNS を受けている人は、一度自分の受け口を確認してみてほしい。
週 1 回、Issue を 1 本立てるだけの運用にする
1 巡目は、かなり好調だった。それならと、そのまま運用に落とした。
仕組みはとても小さい。毎週月曜の朝に GitHub Actions が「探索的テスト」の Issue を 1 本立てる。やるのはそれだけで、探索そのものは、その Issue を開いたセッションで今回と同じ手順を踏む。前回の Issue がまだ開いていれば、新しい Issue は立てない。手が回らない週があっても、Issue が積み上がらない。
今回の手順 (起動の段取り、偽の AWS サーバー、探索担当への指示、仕分けの基準) は、そのまま AI 向けの手順書としてリポジトリに置いた。次の回は、ここまでの試行錯誤をやり直さなくていい。
期待していることもある。これからは、新しいサービスや機能を出す前に、この探索を一度通しておける。作り手の自分には出てこない操作を、出す前に一通り試せる。サービスの質は、かなり上がるはずだと思っている。
心配もある。2 巡目からは、候補の件数は収束していくはずだと見ている。というより、そうでないと困る。毎回 100 件出てくるなら、仕分けが回らない。1 巡目が多かったのは、これまでに溜まっていた分を一気に掘り出したからで、ここから先は差分だけが出てくる。そうなってくれるかどうかは、次の月曜からわかる。
まとめると、こういう話だった。
- 作り手は、自分のサービスを正常系でしか触れない。 探索的テストが苦手なら、その苦手は AI に渡せる
- AI の報告は仮説として扱う。 起票するかは人が決める。思い込みは根拠のコードやログに当たらせて正す
- SNS の HTTPS 受け口は、署名だけでなくトピックも確かめる。 署名は「SNS から来た」ことしか証明しない