【Python】【Django】检测CSRF令牌错误的测试

总结

有时手动创建Django模板表单时会犯一个错误,那就是忘记添加CSRF令牌标签。通常通过在runserver中运行测试环境来检测,但我尝试确认在单元测试中是否可以检测到这个错误。

目录

    1. 源代码

 

    1. 示例应用程序

 

    1. 配置(项目设置)

 

    1. 测试

 

    1. 测试执行结果

 

    1. 获得的见解

 

    再进一步

请将以下内容以中文本地语言改写:

源代码

在Django测试框架中,检测到CSRF令牌错误。

config

プロジェクト設定

sample

フォームで入力したメッセージを保存するだけのサンプルアプリケーション

templates

HTMLテンプレート

tests

ユニットテスト
sample:django.test.testcases.TestCaseを用いたテスト
e2e:django.test.testcases.LiveServerTestCaseとSelenium WebDriverを用いたテスト

示例应用程序

样例模型/sample/models.py

这个模型只是简单地保留消息。

from django.db import models

# Create your models here.
class Sample(models.Model):
    message = models.CharField(verbose_name='メッセージ', max_length=255)

    """サンプルアプリケーションモデル"""
    class Meta:
        # テーブル名
        db_table = 'sample'

表单样本/表单.py

使用Python中的Django框架,通过在样本/models.py文件中定义的Sample模型和django.models.ModelForm,来定义用于消息的表单。

from django import forms
from .models import Sample

class SampleForm(forms.ModelForm):
    """サンプルフォーム"""
    class Meta:
        model = Sample
        fields = ('message',)
        widgets = {
            'message': forms.Textarea(attrs={'placeholder': 'メッセージ'})
        }

查看 sample/views.py

使用在sample/forms.py中定义的SampleForm,来定义一个视图类。

from django.shortcuts import render,redirect
from .forms import SampleForm
from django.urls import reverse
from django.views import View

class SampleFormView(View):

    # Create your views here.
    def get(self, request, *args, **kwargs):
        context = {
            'form': SampleForm()
        }
        return render(request, 'sample/index.html', context)

    def post(self, request, *args, **kwargs):
        form = SampleForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect(reverse('sample:index'))

        context = {
            'form': form
        }
        return render(request, 'sample/index.html', context)

sampleFromView = SampleFormView.as_view()

HTML模板 templates/sample/index.py

只需显示输入表单和“提交”提交按钮。
这次是为了测试CSRF令牌错误,故意去除了{{ csrf_token }}。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>Form Sample</title>
    </head>
    <body>
        <form method="POST" action="{% url 'sample:index' %}">
            {% for field in form %}
            <label>{{ field.label_tag }}</label>
            {{ field }}
            {% endfor %}
            <input type="submit" value="送信" />
        </form>
    </body>
</html>

应用内的URL设置 sample/urls.py

定義访问已创建的视图类的URL。

from django.urls import path
from . import views

app_name='sample'
urlpatterns = [
    path('', views.sampleFromView, name="index")
]

配置(项目设置)

项目设置 config/settings.py

我会添加以下设置。

    • 作成したsampleアプリケーションをインストールする

 

    • HTMLテンプレートをプロジェクト直下のtemplatesディレクトリから読み込む

 

    • 言語設定を「日本語」に設定する

 

    タイムゾーン設定を「東京」に設定する
省略
# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'sample',
]

……

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates')
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

……

# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/

LANGUAGE_CODE = 'ja-JP'

TIME_ZONE = 'Asia/Tokyo'

省略

项目的URL设置 config/urls.py

我已经加载了在sample项目中创建的URL设置,并进行了配置,以便在首页上可以访问。

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('sample.urls')),
]

考试

示例

我将使用django.test.testcases.Testcase来进行测试。为了进行CSRF令牌的检查,我将重新设置self.client对象,该对象是django.test.Client类的一个实例。我会执行GET和POST在示例应用程序中,以确认在GET请求时,指定的模板被正确显示,在POST请求时,HTTP状态码变为403。

参考页面:
请求的创建:测试工具 | Django 文档 | Django
SimpleTestCase:django.test.testcases | Django 文档 | Django

测试/样本/测试视图.py

from django.test.testcases import TestCase
from django.urls import reverse
from django.test.client import Client

class SampleViewTest(TestCase):

    # def _pre_setup(self):
    #     super()._pre_setup()
    #     self.client = Client(enforce_csrf_checks=True)

    def setUp(self):
        super().setUp()
        self.client = Client(enforce_csrf_checks=True)

    def test_get_index_01(self):
        response = self.client.get(reverse('sample:index'))
        self.assertTemplateUsed(response, 'sample/index.html')

    def test_post_index_01(self):
        response = self.client.post(reverse('sample:index'), data={})
        # If csrf_token was template given.
        # self.assertTemplateUsed(response, 'sample/index.html')
        # If csrf_token was't template given.
        self.assertEquals(403, response.status_code)

    def test_post_index_02(self):
        response = self.client.post(reverse('sample:index'), data={'message': 'Test Message'})
        # If csrf_token was template given.
        # self.assertRedirects(response, reverse('sample:index'))
        # If csrf_token was't template given.
        self.assertEquals(403, response.status_code)

端到端

我们将使用Django.test.testcases.LiveServerTestCase和Selenium WebDriver进行测试。

测试/e2e/test_index.py

from django.test.testcases import LiveServerTestCase
import chromedriver_binary
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class LiveServerIndexTest(LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        options = Options()
        options.add_argument('--headless')
        cls.selenium = webdriver.Chrome(options=options)
        cls.selenium.implicitly_wait(10)

    def test_index_01(self):
        self.selenium.get('%s%s' % (self.live_server_url, '/'))
        self.assertTemplateUsed('sample/index.html')
        self.assertEquals('Form Sample', self.selenium.title)

    def test_index_02(self):
        self.selenium.get('%s%s' % (self.live_server_url, '/'))
        message_elem = self.selenium.find_element_by_css_selector('form textarea[name="message"]')
        message_elem.send_keys("Test Message")
        submit_elem = self.selenium.find_element_by_css_selector('form input[type="submit"]')
        submit_elem.click()
        WebDriverWait(self.selenium, 15).until(EC.visibility_of_all_elements_located)

        # assert Submit Success
        # self.assertEquals('Form Sample', self.selenium.title)

        # assert Submit 403 Error(CSRF Token Error)
        self.assertTrue('403' in self.selenium.title)

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()

测试的执行结果

スクリーンショット 2019-08-20 18.08.56.png
スクリーンショット 2019-08-20 18.09.33.png

获得的见解 de

django.test.testcases.TestCaseを用いたユニットテストで、CSRFトークンエラーを検知することは可能

ただし、_pre_setup()メソッド、setUp()メソッドをオーバーライドし、self.clientをCSRFトークンチェック有効(enforce_csrf_checks=True)のクライアントに上書きする必要がある

self.client自体はdjango.test.testcases.TestCaseクラスの親クラスであるdjango.test.testcases.SimpleTestCaseで定義されている

django.test.Clientのenforce_csrf_checks引数はデフォルトでFalseであるため、django.test.testcases.SimpleTestCaseの_pre_setup()メソッドで生成されるself.clientオブジェクトは常にCSRFトークンチェック無効になっている

このため、テストクラス内で_pre_setup()メソッドsetUp()メソッドをオーバーライドし、self.clientをenforce_csrf_checks=Trueであるクライアントオブジェクトで上書きする必要が生じる

LiveServerTestCaseとSelenium WebDriverを用いたユニットテストで、CSRFトークンエラーを検知することは可能

Selenium WebDriverではHTTPステータスコードを取得することはできないため、画面のタイトル等を用いて403エラーを検知する必要がある

再向前迈出一步

我认为可以将在单元测试中检测到CSRF令牌错误的泛化操作大致分为以下几个方向。

django.test.testcases.TestCaseを拡張したベースクラスを使用する

このクラスからテストクラスを作成すれば、無条件でCSRFトークンエラーを検出できるようにする
ベースのテストクラスへの拡張性はあるが、クライアントそのものへの拡張性は低い

テストクラスで用いるクライアントをCSRFトークンエラー検知有効なクライアントに設定する

テストクラスのclient_classにこのクライアントを設定すればCSRFトークンエラー検知有効なクライントを使用するようにする
CSRFトークンエラーを検知したいテストクラスが複数ある場合は、すべてのクラスのclient_classにこのクラスを設定する必要がある

上記2つの合せ技

クライアント、ベースのテストクラス双方に拡張性を持つことができる
テストクラス周りのベース部分が複雑化する可能性がある

在tests/sample/test_views_extend_class.py中使用了扩展了django.test.testcases.TestCase的基类。

CsrfErrorDetactionTestCase:
setUp()メソッドでself.clientをenforce_csrf_checks=Trueを設定したdjango.test.Clientのインスタンスに上書きするdjango.test.testcases.TestCaseの拡張クラス

SampleViewTest:上記のCsrfErrorDetactionTestCaseをベースに作成したビューのテスト

在测试类中,将客户端设置为启用CSRF令牌错误检测的客户端 tests/sample/test_views_extend_client.py

CsrfErrorDetectionClient:enforce_csrf_checksをデフォルトでTrueに設定するdjango.test.Client

SampleViewTest:上記のCsrfErrorDetectionClientをclient_classに設定したビューのテスト

bannerAds