TT Lab
はじめる
学ぶ 学習パス コース

テストツール実戦

CORSと認証の境界をテストする:設計原理

TT Labで続きを見る

一言でいうと

許可されるプリフライトと拒否されるプリフライトを比較し、CORSを認証と誤解するリグレッションを防ぎます。

なぜ必要なのか

許可されていないOriginからのリクエストがサーバーで実行されたため、開発者はCORSライブラリのバグだと判断しました。しかし、ブラウザーの読み取り制限と、サーバーの権限検査は、別の責務でした。テスト名とアサーションが何を保証するのかを、正確に区別する必要があります。

どう動くのか

Originを、pathのないオリジンとして検証し、重複とワイルドカードのポリシーをテストします。OPTIONSリクエストのAccess-Control-Request-Methodを直接設定します。通常のリクエストとプリフライトのステータスと許可ヘッダーを比較しながら、TestClientがブラウザーのブロックを実装していないという限界も記録します。

학생 테스트 → 정상 구현: 실제 시험 모두 통과
           └→ 계약 위반 구현: 해당 동작에서 실패
수집 실패·0개 실행·강제 종료 ≠ 결함 검출

契約を読んで失敗を予測するワークシート

以下は、実装を丸ごと暗記するための答案ではなく、ステップごとのコードレビューです。各変更の断片は、意図的に契約を破っています。変更後でも、正常なケースが成功することがある点に注意してください。実行する前に、どの入力・例外・状態を観測すれば違いが表れるかを予想し、実装した後で、その予想と結果を比較します。

1. オリジンの形式を検証する(テスト)

提供されたservice.pyの次の公開契約をテストしてください: origin(value)は、httpまたはhttpsのURLで、hostがあり、path・query・fragment・ユーザー情報がなければ、入力の文字列を返します。それ以外はValueErrorです。末尾の/もpathなので拒否します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

判断の根拠: URL全体をオリジンとして許可すると、パスやユーザー情報と混同してしまうことがあります。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

レビューする誤った変更の断片:

or url.query

この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。

2. 重複したオリジンを除去する(テスト)

提供されたservice.pyの次の公開契約をテストしてください: origins(values)は、各項目をoriginで検証した後、最初に出てきた順序で重複を除去した新しいリストです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

判断の根拠: 許可リストは、文字列の部分一致ではなく、正確なオリジンのリストです。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

レビューする誤った変更の断片:

[origin(value) for value in values]

この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。

3. メソッドを許可リストで制限する(テスト)

提供されたservice.pyの次の公開契約をテストしてください: methods(values)は、GET・POST・PUT・DELETE・OPTIONSだけを許可し、大文字に変換して重複を除去します。空のリストやそれ以外の値はValueErrorです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

判断の根拠: 許可していないPATCHや任意のメソッドを、黙って追加しません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

レビューする誤った変更の断片:

"DELETE","OPTIONS","PATCH"

この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。

4. 資格情報とアスタリスクを同時に許可しない(テスト)

提供されたservice.pyの次の公開契約をテストしてください: policy(allowed, credentials)は、credentialsがboolであることを確認します。allowedに'*'があればValueErrorで、{allow_origins:origins(allowed), allow_credentials:credentials}を返します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

判断の根拠: このラボの明示的なポリシーは、資格情報の有無に関係なく、アスタリスクを受け付けません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

レビューする誤った変更の断片:

not isinstance(credentials, (bool, int))

この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。

5. 本物のCORSミドルウェアを付ける(テスト)

提供されたservice.pyの次の公開契約をテストしてください: create_app(allowed, credentials=True)は、policyを検証し、CORSMiddlewareを設定したアプリです。GET/POSTだけを許可し、Content-TypeとX-Request-IDのリクエストヘッダーを許可し、X-Traceレスポンスヘッダーをexposeします。GET /dataは{ok:True}とX-Trace='trace-1'を返します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

判断の根拠: プリフライトと実際のレスポンスに、ヘッダーを手で別々に付けると、2つのポリシーが簡単にずれてしまいます。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

レビューする誤った変更の断片:

expose_headers=[]

この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。

6. プリフライトのリクエストを作る(テスト)

提供されたservice.pyの次の公開契約をテストしてください: preflight_headers(source, method, requested='X-Request-ID')は、Origin、Access-Control-Request-Method、Access-Control-Request-Headersの3つのキーを持つ辞書です。methodは大文字です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

判断の根拠: 実際のリクエストメソッドはOPTIONSで、検査したいメソッドは別のヘッダーにあります。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

レビューする誤った変更の断片:

method.lower()

この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。

7. 拒否の行列を計算する(テスト)

提供されたservice.pyの次の公開契約をテストしてください: preflight_status(app, source, method, requested='X-Request-ID')は、TestClientで/dataにOPTIONSリクエストを送り、HTTPステータスを返します。別のオリジン・DELETE・X-Secretヘッダーは400になる必要があります。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

判断の根拠: 拒否の理由3種類を1つのリクエストに混ぜないようにして初めて、欠けているポリシーを見つけられます。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

レビューする誤った変更の断片:

client.get("/data",

この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。

8. CORSと認証の違いを観察する(テスト)

提供されたservice.pyの次の公開契約をテストしてください: cors_observation(app, source)は、GET /dataを送り、(ステータス、Access-Control-Allow-Originの値またはNone、JSON本文)を返します。許可されていないオリジンでも200の本文は実行されますが、許可オリジンのヘッダーはない必要があります。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

判断の根拠: curlやサーバー間のリクエストは、ブラウザーのCORSによる読み取り制限に従いません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

レビューする誤った変更の断片:

source

この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。

現場での姿

TestClientはブラウザーではありません。CORSのレスポンスヘッダーとプリフライトは検査しますが、ブラウザー自体の読み取りブロックまでは実装していません。許可されていないOriginを送った通常のGETも、サーバーで実行されることがあります。機密性の高い操作は、別の認証・権限・CSRFのポリシーで保護する必要があります。提供された実装は読んでもかまいませんが、採点は別のコピーを使用します。ソースの文言の検査やファイルの修正で欠陥を回避せず、公開インターフェースの実行結果を検査してください。

次のラボですること

8つのステップが、1つの実行可能な成果物につながります。オリジンの形式を検証する(テスト) → 重複したオリジンを除去する(テスト) → メソッドを許可リストで制限する(テスト) → 資格情報とアスタリスクを同時に許可しない(テスト) → 本物のCORSミドルウェアを付ける(テスト) → プリフライトのリクエストを作る(テスト) → 拒否の行列を計算する(テスト) → CORSと認証の違いを観察する(テスト)。

各ステップでは、関数やファイルが存在するという事実ではなく、実際の戻り値・例外・状態の変化を検査します。正解を見たあとは、わざと境界の比較や後始末のコードを変えて、どのテストが失敗するかを確認してください。前のテストが次のステップでも維持される理由を説明し、このラボが保証しない本番の条件を1つ書いてみてください。