はじめに
修正した内容は、後の変更で意図せず元に戻ってしまうことがあります。セキュリティ回帰テストは、重要な要件を、繰り返し実行できるリクエストと期待する結果に変えます。
このラボでは、保護された DeskLite エンドポイント向けの小さな Python テンプレートを拡張します。その後、意図的に不具合を入れた 2 つのローカルサービスに対して、テスト自体を確認します。テストフレームワークのインストールや、テスト作成の経験は不要です。
保護されたエンドポイントを試す
このステップでは、テストで守るべき動作を確認します。
用意された正しいサービスは、ローカルポート 8080 で動作しています。その /tickets エンドポイントは、有効な Token には元のチケットを返し、Token がない場合や誤っている場合には、401 とエラーオブジェクトを返します。認証と業務レスポンスは別々の要件です。ステータス 200 だけでは、正しいチケットが返ったことは証明できません。
cd /home/labex/project/ticket-tests
このラボで公開されている course_test_fixture_token は、意図的に作った偽物の公開テスト用データです。これらの独立した学習用サービスだけで受け入れられ、外部での効力はありません。前に学んだ、アプリケーションの非公開認証情報の代わりになるものではありません。
Token のラボと同じ curl のリクエストヘッダーオプション -H を使います。
curl -i -H 'Authorization: Bearer course_test_fixture_token' http://127.0.0.1:8080/tickets
HTTP 200 と {"tickets": [{"id": 1, "title": "Printer offline"}]} が返るはずです。-i は、ステータスを含む HTTP レスポンスヘッダーを表示します。
今度は、ヘッダーを完全に省略します。
curl -i http://127.0.0.1:8080/tickets
HTTP 401 と {"error": "unauthorized"} が返るはずです。この拒否は必要な動作であり、演習が壊れているわけではありません。
小さなテストテンプレートを読み取る
このステップでは、テストの各行を、実際のリクエストの各部分に関連付けます。
cat test_security.py
用意されたファイルの内容は次のとおりです。
from api import request
def test_valid_token():
response = request("/tickets", {"Authorization": "Bearer course_test_fixture_token"})
assert response["status"] == 200
assert response["body"] == {"tickets": [{"id": 1, "title": "Printer offline"}]}
from api import request は、用意されたリクエスト用ヘルパーを読み込みます。def test_valid_token(): は、名前の付いたテスト関数を定義します。前のラボの検索関数と同様に、字下げされた行はその関数に属します。
request(path, headers) は実際の HTTP リクエストを送信し、名前と値を対応付ける Python の辞書を返します。ヘッダーの対応付けにも波括弧を使います。{"Authorization": "Bearer ..."} はヘッダーを 1 つ送信し、{} は何も送信しません。response["status"] はステータスの項目を読み取り、response["body"] は解析済みの JSON 本文を読み取ります。
アサーションは、期待する条件を表します。Python の == は 2 つの値を比較するもので、値を代入する単一の = とは異なります。assert の条件が偽になると AssertionError が発生し、テストが失敗します。最後の行は、チケットのタイトルも含む、期待する業務オブジェクト全体を比較します。
ヘルパーは、実行用プログラムの対象引数から、用意されたサービスを選びます。編集するのはテストであり、ヘルパーやサービスではありません。
合格するテストの不足を見つける
このステップでは、成功ケースのテストが合格するだけでは不十分な理由を確認します。
用意されたテスト実行用プログラムを実行します。引数 good は正しいサービスを選びます。
python3 run_tests.py good
test_valid_token に "result": "PASS" と表示されるはずです。実行用プログラムは、名前が test_ で始まる関数を集め、それぞれを呼び出します。すべて合格なら終了コード 0、アサーションの失敗なら 1、テストや実行時のエラーなら 2 を返します。
他の 2 つのサービスには、意図的に不具合があります。missing-auth は Token のないリクエストを受け入れ、wrong-auth は誤った Token を受け入れます。どちらも成功ケースのレスポンスは引き続き機能します。両方を試します。
python3 run_tests.py missing-auth
python3 run_tests.py wrong-auth
どちらも、唯一の既存テストに合格したままです。この結果は、テストのカバー範囲に不足があることを示しています。テストは拒否されるべきリクエストを送っていないため、どちらの認証回避にも気付けませんでした。
Token がない場合の要件を追加する
このステップでは、認証のない実際のリクエストが拒否されることを期待するテストを 1 つ追加します。
テストファイルを開きます。
nano test_security.py
成功ケースのテストを残し、その下に新しい関数を追加します。ファイル全体は次のようになるはずです。
from api import request
def test_valid_token():
response = request("/tickets", {"Authorization": "Bearer course_test_fixture_token"})
assert response["status"] == 200
assert response["body"] == {"tickets": [{"id": 1, "title": "Printer offline"}]}
def test_missing_token():
response = request("/tickets", {})
assert response["status"] == 401
assert response["body"] == {"error": "unauthorized"}
空の対応表 {} は、リクエストヘッダーがないことを意味します。ステータスのアサーションは拒否を確認し、本文のアサーションはエラーレスポンスにチケットデータが含まれないことを確認します。空白 4 つの字下げを維持してください。Ctrl+O、Enter で保存し、Ctrl+X で終了します。
python3 run_tests.py good
両方のテストが合格するはずです。次に、ヘッダーがない場合に意図的な不具合を持つサービスを使います。
python3 run_tests.py missing-auth
test_missing_token に "result": "FAIL" と "error": "AssertionError" が表示されるはずです。この意図した終了コード 1 は有用な証拠です。新しいテストは、拒否すべきリクエストをサービスが受け入れたことを検出しています。インポートエラーやサーバーへの接続エラーとは異なります。
誤った Token の要件を追加する
このステップでは、存在しているものの無効な認証情報を扱います。
ヘッダーは、存在していても有効とは限りません。ヘッダーがない場合だけをテストすると、空でない Token を何でも受け入れるサービスを見逃します。同じファイルを開き、最初の 2 つを維持したまま、3 つ目の関数を追加します。
nano test_security.py
次の完全なファイルを使います。
from api import request
def test_valid_token():
response = request("/tickets", {"Authorization": "Bearer course_test_fixture_token"})
assert response["status"] == 200
assert response["body"] == {"tickets": [{"id": 1, "title": "Printer offline"}]}
def test_missing_token():
response = request("/tickets", {})
assert response["status"] == 401
assert response["body"] == {"error": "unauthorized"}
def test_wrong_token():
response = request("/tickets", {"Authorization": "Bearer wrong_fixture_token"})
assert response["status"] == 401
assert response["body"] == {"error": "unauthorized"}
wrong_fixture_token は、正しいサービスに意図的に受け入れられない値です。Ctrl+O、Enter、Ctrl+X で保存して終了します。
python3 run_tests.py good
3 つのテストすべてが合格するはずです。次に、誤ったヘッダーを受け入れる回帰をテストします。
python3 run_tests.py wrong-auth
test_wrong_token だけが AssertionError で失敗するはずです。この不具合のあるサービスを合格させるために、401 を 200 に変更しないでください。それでは、セキュリティ要件を取り除いてしまいます。
両方の回帰の検出を検証する
このステップでは、完成したテスト一式を、3 つすべての実装に対して確認します。
python3 run_tests.py good
python3 run_tests.py missing-auth
python3 run_tests.py wrong-auth
個々の結果を読んでください。正しいサービスは、3 つすべてに合格する必要があります。ヘッダーがない場合の回帰では test_missing_token だけが失敗し、誤ったヘッダーの場合の回帰では test_wrong_token だけが失敗する必要があります。有効な Token による業務テストは、どこでも引き続き合格する必要があります。
これらの比較実行での失敗は意図したものです。構文エラー、接続エラー、無条件の失敗では、検出できたことを示せません。コースのチェックでは、テストが元の各サービスに 3 種類の実際の HTTP リクエストを送ったことも確認します。
この比較が対象とするのは、これら 2 つの既知の回帰です。認証や認可に関する、あらゆる性質を証明するものではありません。
正しいサービスで合格する実行を最後に行う
このステップでは、意図した実装に対して繰り返し実行できるテストコマンドで締めくくります。
cat test_security.py
元の成功ケースのテストと、両方の拒否テストが残っていることを確認してください。それぞれの名前は、3 つの独立した要件を表しています。
python3 run_tests.py good
3 つの結果すべてが PASS になるはずです。今後のリリース判定では、意図したサービスに対して同じコマンドを実行できます。意図的に不具合を入れたサービスは、学習用の比較対象として残ります。提供すべき実装ではありません。テスト範囲について文書で主張するのではなく、実際の動作を守ることができました。
まとめ
明示的な認証要件を、3 つの実際の HTTP テストに結び付けました。正しい実装は合格し、用意された各認証回避は、対応する拒否のアサーションで検出されます。この比較は、テストが単にエラーなく実行されるだけでなく、動作を守っている証拠になります。



