From e452e08f09bf36282457dd6a48d65ec5e2fed0cb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 26 Dec 2017 11:45:56 -0600 Subject: [PATCH 1/4] Add fixtures for user request factories, clients Fixes #565 This is based on conversations in issues #553, #554, and #284 - Introduces a django_user fixture, which is a user with no additional priveleges. Similar to admin_user. - A user_client, based on django_user. Similar to admin_client. - 3 new RequestFactory based fixtures, for unauthenticated, authenticated, and admin users: - rf_unauth: relies on AnonymousUser, similar to django docs example. - rf_admin: relies on admin_user fixture - rf_user: relies on new django_user fixture In addition, these 3 fixtures differ from rf in that they: - Mimic AuthenticationMiddleware by add the user attribute to the request object - Mimic SessionMiddleware by adding the 'session' attribute to the request object. This is an in-memory session store object from ``django.contrib.sessions.backends.base.SessionBase``. This fills the gaps left over by not having normal and unauthenticated user objects, clients, and request factories (when admin ones were available), and also covers a common case likely present in Django projects - anonymous (not logged in), authenticated, and admin users. Also, Update fixtures.py with changes from @blueyed --- pytest_django/fixtures.py | 103 +++++++++++++++++++++++++++++++++++++- pytest_django/plugin.py | 5 ++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index cc6a19dca..d5728bcb6 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -17,7 +17,8 @@ __all__ = ['django_db_setup', 'db', 'transactional_db', 'admin_user', 'django_user_model', 'django_username_field', - 'client', 'admin_client', 'rf', 'settings', 'live_server', + 'client', 'admin_client', 'user_client', 'rf', 'settings', + 'django_user', 'rf_unauth', 'rf_user', 'rf_admin', 'live_server', '_live_server_helper', 'django_assert_num_queries'] @@ -229,6 +230,36 @@ def admin_client(db, admin_user): return client +@pytest.fixture() +def django_user(db, django_user_model, django_username_field): + """A Django user. + This uses an existing user with username "user", or creates a new one with + password "password". + """ + UserModel = django_user_model + username_field = django_username_field + + try: + user = UserModel._default_manager.get(**{username_field: 'user'}) + except UserModel.DoesNotExist: + extra_fields = {} + if username_field != 'username': + extra_fields[username_field] = 'user' + user = UserModel._default_manager.create_user( + 'user', 'user@example.com', 'password', **extra_fields) + return user + + +@pytest.fixture() +def user_client(db, django_user): + """A Django test client logged in as a normal user.""" + from django.test.client import Client + + client = Client() + client.login(username=django_user.username, password='password') + return client + + @pytest.fixture() def rf(): """RequestFactory instance""" @@ -239,6 +270,76 @@ def rf(): return RequestFactory() +@pytest.fixture +def rf_unauth(): + """Anonymous user request factory. + + This does two things to the request object: + + 1. Adds an anonymous user per Django's doc examples on request + factories: + + https://docs.djangoproject.com/en/2.0/topics/testing/advanced/#example + + This simiulates ``django.contrib.auth.middleware.AuthenticationMiddleware``. + + 2. Adds a naive session storage object to request. + + This simiulates ``django.contrib.sessions.middleware.SessionMiddleware``, + and prevents errors on stuff like SessionWizardView in django-formtools. + """ + from django.test.client import RequestFactory + from django.contrib.auth.models import AnonymousUser + from django.contrib.sessions.backends.base import SessionBase + + class UnauthRequestFactory(RequestFactory): + def request(self, **request): + r = super(UnauthRequestFactory, self).request(**request) + r.user = AnonymousUser() + r.session = SessionBase() + return r + + return UnauthRequestFactory() + + +@pytest.fixture +def rf_admin(admin_user): + """Admin user request facctory. + + Mimics AuthenticationMiddleware. Also adds a memory-backed session store + to the request object.""" + from django.test.client import RequestFactory + from django.contrib.sessions.backends.base import SessionBase + + class AdminRequestFactory(RequestFactory): + def request(self, **request): + r = super(AdminRequestFactory, self).request(**request) + r.user = admin_user + r.session = SessionBase() + return r + + return AdminRequestFactory() + + +@pytest.fixture +def rf_user(django_user): + """Normal user request factory. + + Mimics AuthenticationMiddleware. Also adds a memory-backed session store + to the request object.""" + from django.test.client import RequestFactory + from django.contrib.sessions.backends.base import SessionBase + + class UserRequestFactory(RequestFactory): + def request(self, **request): + r = super(UserRequestFactory, self).request(**request) + r.user = django_user + r.session = SessionBase() + return r + + return UserRequestFactory() + + class SettingsWrapper(object): _to_restore = [] diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 1585c6983..2813ca6b2 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -22,6 +22,7 @@ from .fixtures import django_db_createdb # noqa from .fixtures import django_db_modify_db_settings # noqa from .fixtures import django_db_modify_db_settings_xdist_suffix # noqa +from .fixtures import django_user # noqa from .fixtures import _live_server_helper # noqa from .fixtures import admin_client # noqa from .fixtures import admin_user # noqa @@ -31,8 +32,12 @@ from .fixtures import django_username_field # noqa from .fixtures import live_server # noqa from .fixtures import rf # noqa +from .fixtures import rf_admin # noqa +from .fixtures import rf_user # noqa +from .fixtures import rf_unauth # noqa from .fixtures import settings # noqa from .fixtures import transactional_db # noqa +from .fixtures import user_client # noqa from .pytest_compat import getfixturevalue from .lazy_django import django_settings_is_configured, skip_if_no_django From 67f2225b31d823d2fd854d4f4587e9e801e7ae7d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 26 Dec 2017 12:07:02 -0600 Subject: [PATCH 2/4] add tests for new fixtures --- tests/test_fixtures.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 10ceac35c..3879043de 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -11,6 +11,7 @@ import pytest from django.db import connection, transaction +from django.contrib.sessions.backends.base import SessionBase from django.conf import settings as real_settings from django.core import mail from django.test.client import Client, RequestFactory @@ -33,25 +34,70 @@ def test_admin_client(admin_client): assert force_text(resp.content) == 'You are an admin' +@pytest.mark.django_db +def test_user_client(user_client): + assert isinstance(user_client, Client) + resp = user_client.get('/admin-required/') + assert force_text(resp.content) == 'Access denied' + + def test_admin_client_no_db_marker(admin_client): assert isinstance(admin_client, Client) resp = admin_client.get('/admin-required/') assert force_text(resp.content) == 'You are an admin' +def test_user_client_no_db_marker(user_client): + assert isinstance(user_client, Client) + resp = user_client.get('/admin-required/') + assert force_text(resp.content) == 'Access denied' + + @pytest.mark.django_db def test_admin_user(admin_user, django_user_model): assert isinstance(admin_user, django_user_model) +@pytest.mark.django_db +def test_django_user(django_user, django_user_model): + assert isinstance(django_user, django_user_model) + + def test_admin_user_no_db_marker(admin_user, django_user_model): assert isinstance(admin_user, django_user_model) +def test_django_user_no_db_marker(django_user, django_user_model): + assert isinstance(django_user, django_user_model) + + def test_rf(rf): assert isinstance(rf, RequestFactory) +def test_rf_user(rf_user, django_user_model): + assert isinstance(rf_user, RequestFactory) + request = rf_user.get('/admin-required/') + assert isinstance(request.user, django_user_model) + assert isinstance(request.session, SessionBase) + + +def test_rf_admin(rf_admin, django_user_model): + assert isinstance(rf_admin, RequestFactory) + request = rf_admin.get('/admin-required/') + assert isinstance(request.user, django_user_model) + assert isinstance(request.session, SessionBase) + + +def test_rf_unauth(rf_unauth, django_user_model): + assert isinstance(rf_unauth, RequestFactory) + request = rf_unauth.get('/admin-required/') + assert hasattr(request, 'user') + # AnonymousUser is a plain-object (duck-typed) + assert not isinstance(request.user, django_user_model) + assert isinstance(request.session, SessionBase) + + @pytest.mark.django_db def test_django_assert_num_queries_db(django_assert_num_queries): with django_assert_num_queries(3): From 5ff887fa6754dec66ab8b9b57b7bb48ea15b1cad Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 26 Dec 2017 13:27:40 -0600 Subject: [PATCH 3/4] add documentation for new fixtures Update helpers.rst with corrections from @blueyed --- docs/helpers.rst | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/helpers.rst b/docs/helpers.rst index 47e1ad76a..1155a10ee 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -121,6 +121,39 @@ Example response = my_view(request) assert response.status_code == 200 +``rf_user``, ``rf_admin``, ``rf_unauth`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Additional instances of RequestFactory, which mimic the behavior of these +two middlewares: + +- `django.contrib.sessions.middleware.SessionMiddleware`_ +- `django.contrib.auth.middleware.AuthenticationMiddleware`_ + +This will add two attributes to the ``request`` object, +``request.user`` and ``request.session``. + +- ``request.session`` is an in-memory `SessionBase`_ object. +- ``request.user`` depends on the fixture being used: + + - ``rf_unauth``: an `AnonymousUser`_ + - ``rf_user``: a normal user with no additional privileges + - ``rf_admin``: an admin user + +:: + + from myapp.views import my_view + + def test_details(rf_unauth): + request = rf_unauth.get('/customer/details') + response = my_view(request) + assert response.status_code == 200 + +.. _django.contrib.sessions.middleware.SessionMiddleware: https://docs.djangoproject.com/en/2.0/ref/middleware/#django.contrib.sessions.middleware.SessionMiddleware +.. _django.contrib.auth.middleware.AuthenticationMiddleware: https://docs.djangoproject.com/en/2.0/ref/middleware/#django.contrib.auth.middleware.AuthenticationMiddleware +.. _SessionBase: https://docs.djangoproject.com/en/2.0/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase +.. _AnonymousUser: https://docs.djangoproject.com/en/2.0/ref/contrib/auth/#django.contrib.auth.models.AnonymousUser + ``client`` - ``django.test.Client`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -167,6 +200,20 @@ Example Using the `admin_client` fixture will cause the test to automatically be marked for database use (no need to specify the ``django_db`` mark). +``user_client`` - ``django.test.Client`` logged in as normal user +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Same as ``admin_client``, but for a regular user. + +Example +""""""" + +:: + + def test_an_admin_view(user_client): + response = user_client.get('/admin/') + assert response.status_code == 403 + ``admin_user`` - a admin user (superuser) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -177,6 +224,11 @@ Using the `admin_user` fixture will cause the test to automatically be marked fo ``django_db`` mark). +``django_user`` - a normal user +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Same as ``admin_user``, but for a regular user. + ``django_user_model`` ~~~~~~~~~~~~~~~~~~~~~ From 0f7f6e964ab0eb288032e07f43c9be80c8f2ea97 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 18 Jan 2018 08:16:28 -0600 Subject: [PATCH 4/4] use django_ prefix for *new* fixtures --- docs/helpers.rst | 20 ++++++++++---------- pytest_django/fixtures.py | 17 +++++++++-------- pytest_django/plugin.py | 8 ++++---- tests/test_fixtures.py | 30 +++++++++++++++--------------- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 1155a10ee..a4eba3f9e 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -121,8 +121,8 @@ Example response = my_view(request) assert response.status_code == 200 -``rf_user``, ``rf_admin``, ``rf_unauth`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``django_rf_user``, ``django_rf_admin``, ``django_rf_unauth`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Additional instances of RequestFactory, which mimic the behavior of these two middlewares: @@ -136,16 +136,16 @@ This will add two attributes to the ``request`` object, - ``request.session`` is an in-memory `SessionBase`_ object. - ``request.user`` depends on the fixture being used: - - ``rf_unauth``: an `AnonymousUser`_ - - ``rf_user``: a normal user with no additional privileges - - ``rf_admin``: an admin user + - ``django_rf_unauth``: an `AnonymousUser`_ + - ``django_rf_user``: a normal user with no additional privileges + - ``django_rf_admin``: an admin user :: from myapp.views import my_view - def test_details(rf_unauth): - request = rf_unauth.get('/customer/details') + def test_details(django_rf_unauth): + request = django_rf_unauth.get('/customer/details') response = my_view(request) assert response.status_code == 200 @@ -200,8 +200,8 @@ Example Using the `admin_client` fixture will cause the test to automatically be marked for database use (no need to specify the ``django_db`` mark). -``user_client`` - ``django.test.Client`` logged in as normal user -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``django_user_client`` - ``django.test.Client`` logged in as normal user +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Same as ``admin_client``, but for a regular user. @@ -210,7 +210,7 @@ Example :: - def test_an_admin_view(user_client): + def test_an_admin_view(django_user_client): response = user_client.get('/admin/') assert response.status_code == 403 diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index d5728bcb6..388149c61 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -16,10 +16,11 @@ from .lazy_django import skip_if_no_django __all__ = ['django_db_setup', 'db', 'transactional_db', 'admin_user', - 'django_user_model', 'django_username_field', - 'client', 'admin_client', 'user_client', 'rf', 'settings', - 'django_user', 'rf_unauth', 'rf_user', 'rf_admin', 'live_server', - '_live_server_helper', 'django_assert_num_queries'] + 'django_user_model', 'django_username_field', 'client', + 'admin_client', 'django_user_client', 'rf', 'settings', + 'django_user', 'django_rf_unauth', 'django_rf_user', + 'django_rf_admin', 'live_server', '_live_server_helper', + 'django_assert_num_queries'] @pytest.fixture(scope='session') @@ -251,7 +252,7 @@ def django_user(db, django_user_model, django_username_field): @pytest.fixture() -def user_client(db, django_user): +def django_user_client(db, django_user): """A Django test client logged in as a normal user.""" from django.test.client import Client @@ -271,7 +272,7 @@ def rf(): @pytest.fixture -def rf_unauth(): +def django_rf_unauth(): """Anonymous user request factory. This does two things to the request object: @@ -303,7 +304,7 @@ def request(self, **request): @pytest.fixture -def rf_admin(admin_user): +def django_rf_admin(admin_user): """Admin user request facctory. Mimics AuthenticationMiddleware. Also adds a memory-backed session store @@ -322,7 +323,7 @@ def request(self, **request): @pytest.fixture -def rf_user(django_user): +def django_rf_user(django_user): """Normal user request factory. Mimics AuthenticationMiddleware. Also adds a memory-backed session store diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 2813ca6b2..da613f9ab 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -32,12 +32,12 @@ from .fixtures import django_username_field # noqa from .fixtures import live_server # noqa from .fixtures import rf # noqa -from .fixtures import rf_admin # noqa -from .fixtures import rf_user # noqa -from .fixtures import rf_unauth # noqa +from .fixtures import django_rf_admin # noqa +from .fixtures import django_rf_user # noqa +from .fixtures import django_rf_unauth # noqa from .fixtures import settings # noqa from .fixtures import transactional_db # noqa -from .fixtures import user_client # noqa +from .fixtures import django_user_client # noqa from .pytest_compat import getfixturevalue from .lazy_django import django_settings_is_configured, skip_if_no_django diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 3879043de..80baa8a2e 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -35,9 +35,9 @@ def test_admin_client(admin_client): @pytest.mark.django_db -def test_user_client(user_client): - assert isinstance(user_client, Client) - resp = user_client.get('/admin-required/') +def test_user_client(django_user_client): + assert isinstance(django_user_client, Client) + resp = django_user_client.get('/admin-required/') assert force_text(resp.content) == 'Access denied' @@ -47,9 +47,9 @@ def test_admin_client_no_db_marker(admin_client): assert force_text(resp.content) == 'You are an admin' -def test_user_client_no_db_marker(user_client): - assert isinstance(user_client, Client) - resp = user_client.get('/admin-required/') +def test_user_client_no_db_marker(django_user_client): + assert isinstance(django_user_client, Client) + resp = django_user_client.get('/admin-required/') assert force_text(resp.content) == 'Access denied' @@ -75,23 +75,23 @@ def test_rf(rf): assert isinstance(rf, RequestFactory) -def test_rf_user(rf_user, django_user_model): - assert isinstance(rf_user, RequestFactory) - request = rf_user.get('/admin-required/') +def test_rf_user(django_rf_user, django_user_model): + assert isinstance(django_rf_user, RequestFactory) + request = django_rf_user.get('/admin-required/') assert isinstance(request.user, django_user_model) assert isinstance(request.session, SessionBase) -def test_rf_admin(rf_admin, django_user_model): - assert isinstance(rf_admin, RequestFactory) - request = rf_admin.get('/admin-required/') +def test_rf_admin(django_rf_admin, django_user_model): + assert isinstance(django_rf_admin, RequestFactory) + request = django_rf_admin.get('/admin-required/') assert isinstance(request.user, django_user_model) assert isinstance(request.session, SessionBase) -def test_rf_unauth(rf_unauth, django_user_model): - assert isinstance(rf_unauth, RequestFactory) - request = rf_unauth.get('/admin-required/') +def test_rf_unauth(django_rf_unauth, django_user_model): + assert isinstance(django_rf_unauth, RequestFactory) + request = django_rf_unauth.get('/admin-required/') assert hasattr(request, 'user') # AnonymousUser is a plain-object (duck-typed) assert not isinstance(request.user, django_user_model)