【Python】【Django】检测CSRF令牌错误的测试
总结
有时手动创建Django模板表单时会犯一个错误,那就是忘记添加CSRF令牌标签。通常通过在runserver中运行测试环境来检测,但我尝试确认在单元测试中是否可以检测到这个错误。
目录
-
- 源代码
-
- 示例应用程序
-
- 配置(项目设置)
-
- 测试
-
- 测试执行结果
-
- 获得的见解
- 再进一步
请将以下内容以中文本地语言改写:
源代码
在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()
测试的执行结果


获得的见解 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に設定したビューのテスト