From 41256cc76d48ad0a3d00141800c0edbda52de1f6 Mon Sep 17 00:00:00 2001 From: Oussama Jarrousse Date: Mon, 25 Dec 2023 17:44:28 +0100 Subject: [PATCH 1/7] initial --- mfa/apps.py | 2 +- mfa/tests.py | 3 -- pytest.ini | 104 +++++++++++++++++++++++++++++++++++++++ requirements_testing.txt | 10 ++++ tests/__init__.py | 0 tests/conftest.py | 0 tests/test_views.py | 2 + tox.ini | 41 +++++++++++++++ 8 files changed, 158 insertions(+), 4 deletions(-) delete mode 100644 mfa/tests.py create mode 100644 pytest.ini create mode 100644 requirements_testing.txt create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_views.py create mode 100644 tox.ini diff --git a/mfa/apps.py b/mfa/apps.py index cb5ecca..8a6e285 100644 --- a/mfa/apps.py +++ b/mfa/apps.py @@ -1,4 +1,4 @@ from django.apps import AppConfig class myAppNameConfig(AppConfig): name = 'mfa' - verbose_name = 'A Much Better Name' \ No newline at end of file + verbose_name = 'Django MFA2' \ No newline at end of file diff --git a/mfa/tests.py b/mfa/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/mfa/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..eea7252 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,104 @@ +[pytest] +# Searching +python_files = test_* +python_classes = Tests* +python_functions = test_* + +env_files = + .env + +# do not search for tests in these folders +norecursedirs = venv + +# Add folder to PYTHONPATH +# requires pytest >= 7.0.0 +pythonpath = mfa + + +# https://pytest-django.readthedocs.io/en/latest/usage.html +; DJANGO_SETTINGS_MODULE = + ; + +# do not override the debug mode (True/False) set in the django settings module +# https://pytest-django.readthedocs.io/en/latest/usage.html#additional-pytest-ini-settings +django_debug_mode = keep + + +# +# set env variables +# https://tech.serhatteker.com/post/2020-02/test-env-vars-in-python/ +# https://github.com/pytest-dev/pytest-env +; env = + ; KEY=value + + +addopts = + # verbose + -v + # more verbosity + # -vv + # Don't show warnings + # -p no:warnings + # generates coverage report + # note that enabling pytest coverage will cause debugging pytest to fail on pycharm + # add the --no-cov to the pytest configuration on pycharm to allow for debugging pytest + --cov=./src + # surpress generating converage if one or more tests failed + ; --no-cov-on-fail + # do not run migrations => faster test initialization + # --nomigrations + # Show hypthesis statistics whereever hypothesis was used + # ignore these tests/files when looking for tests + #--ignore= + # black + # --black + --hypothesis-show-statistics + + +# Define additional pytest markers so that using them in test will not trigger warnings +# To show the help line use: % pytest --marker +# To run pytest on a specifc marker use: pytest -m mark +# to run pytestt on several markers use quotation and logic operators as in: +# pytest -m "mark1 and mark2" +# pytest -m "mark1 or mark2" +# pytest -m "mark1 and not mark2" +markers = + API: tests of server api functions whether it is exposed as REST API or otherwise + BLACK_BOX: Black Box tests + WHITE_BOX: White Box tests + ENVIRONMENT: tests for the environment + CONFIGURATION: tests related configurations + LOGGING: tests related to logging + UNIT: Unit tests + INTEGRATION: Integration testing + UTILS: tests for utilities + FOCUS: tests under the microscope... under the spotlight... in focus + FUNC: functional teesting + REGRESSION: tests for fixed bugs + + DJANGO: tests related to DJANGO + + HTTP_REQUEST: tests of functions that handles HTTP REQUESTS + HTTP_GET: tests of functions that handles HTTP_GET_REQUESTS + HTTP_POST: tests of functions that handles HTTP_POST_REQUESTS + AUTH: tests related to user authentication + SQL_DB: tests related to the sql database + + CLI: tests related to flask-cli + SERVER: tests for the server + + API_V1: API related tests + + PRIVILEGED_USER: tests for privileged users + NON_PRIVILEGED_USER: tests for non-privileged users + PERMISSIONS: tests related to inspectre permissions + + ENDPOINTS: tests for endpoints (API nodes) + SERIALIZERS: tests for serializers + VIEWSETS: tests for DRF viewsets + FILTERS: tests for DRF filters + MODELS: tests for models + VALIDATORS: tests for validators + + ERROR_HANDLING: tests for error handling + SECURITY: tests for security diff --git a/requirements_testing.txt b/requirements_testing.txt new file mode 100644 index 0000000..a07e042 --- /dev/null +++ b/requirements_testing.txt @@ -0,0 +1,10 @@ +tox +pytest>=7.0.0 +pytest-xdist +pytest-cov # Test coverage +pytest-dotenv # plugin to load environment from .env file +pytest-env # plugin to allow passing environment variable to pytest environmentt +pytest-mock # plugin that provides a mocker fixture which is a thin-wrapper around the patching API provided by the mock package +hypothesis # plugin that helps in automatize generating random values instead of static values in pyttests +pytest-django # pytest support for Django +validators # a package containing several validator functions \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 0000000..a1ace31 --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,2 @@ +import pytest + diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..23946b5 --- /dev/null +++ b/tox.ini @@ -0,0 +1,41 @@ +# Tox (https://tox.readthedocs.io/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. +# +# See also https://tox.readthedocs.io/en/latest/config.html for more +# configuration options. + +[tox] +# Choose your Python versions. They have to be available +# on the system the tests are run on. +# comma separated +envlist = py39, py310, py311 + +# Tell tox to not require a setup.py file +;skipsdist = True + +isolated_build = True + +[testenv] +# https://tox.wiki/en/latest/example/basic.html#using-a-different-default-pypi-url +;setenv = +; PIP_INDEX_URL = https://pypi.my-alternative-index.org + +# https://tech.serhatteker.com/post/2020-02/test-env-vars-in-python/ +;setenv = +; NAME=value + +# https://tox.wiki/en/latest/example/basic.html#passing-down-environment-variables +# passenv = ENV_VAR_NAME + +# https://tox.wiki/en/latest/example/pytest.html#extended-example-change-dir-before-test-and-use-per-virtualenv-tempdir +;changedir = tests + +deps = + -rrequirements.txt + +# https://tox.wiki/en/latest/example/basic.html#ignoring-a-command-exit-code +commands = +; pytest --junitxml=report.xml + pytest {posargs} From 6ae4c2508c2faae5ab29c9c459d3173739dd910a Mon Sep 17 00:00:00 2001 From: Oussama Jarrousse Date: Tue, 26 Dec 2023 17:33:21 +0100 Subject: [PATCH 2/7] 2 passed in 0.82s, 32% coverage --- pytest.ini | 16 ++++++------ tests/conftest.py | 19 +++++++++++++++ tests/settings.py | 51 +++++++++++++++++++++++++++++++++++++++ tests/templates/base.html | 11 +++++++++ tests/test_views.py | 23 ++++++++++++++++++ 5 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 tests/settings.py create mode 100644 tests/templates/base.html diff --git a/pytest.ini b/pytest.ini index eea7252..e3e533a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -8,16 +8,16 @@ env_files = .env # do not search for tests in these folders -norecursedirs = venv +norecursedirs = .vscode .tox docs example img mfa venv .coverage django_mfa2.egg-info # Add folder to PYTHONPATH # requires pytest >= 7.0.0 -pythonpath = mfa +pythonpath = . # https://pytest-django.readthedocs.io/en/latest/usage.html -; DJANGO_SETTINGS_MODULE = - ; +DJANGO_SETTINGS_MODULE = tests.settings + # do not override the debug mode (True/False) set in the django settings module # https://pytest-django.readthedocs.io/en/latest/usage.html#additional-pytest-ini-settings @@ -42,7 +42,7 @@ addopts = # generates coverage report # note that enabling pytest coverage will cause debugging pytest to fail on pycharm # add the --no-cov to the pytest configuration on pycharm to allow for debugging pytest - --cov=./src + --cov=./mfa # surpress generating converage if one or more tests failed ; --no-cov-on-fail # do not run migrations => faster test initialization @@ -53,6 +53,8 @@ addopts = # black # --black --hypothesis-show-statistics + # Add --reuse-db if you want to speed up tests by reusing the database between test runs. + #--reuse-db # Define additional pytest markers so that using them in test will not trigger warnings @@ -91,11 +93,11 @@ markers = PRIVILEGED_USER: tests for privileged users NON_PRIVILEGED_USER: tests for non-privileged users - PERMISSIONS: tests related to inspectre permissions + PERMISSIONS: tests related to permissions ENDPOINTS: tests for endpoints (API nodes) SERIALIZERS: tests for serializers - VIEWSETS: tests for DRF viewsets + VIEWS: tests for DRF viewsets FILTERS: tests for DRF filters MODELS: tests for models VALIDATORS: tests for validators diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..583e86d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +import pytest + +# @pytest.fixture +# def api_request(rf): +# request = rf.get('/url') +# # Modify the request object as needed (e.g., set user, add data) +# return request + +# @pytest.fixture +# def create_test_model(db): +# def make_model(**kwargs): +# return MyModel.objects.create(**kwargs) +# return make_model + +@pytest.fixture +def authenticated_user(client, django_user_model): + user = django_user_model.objects.create_user(username='test', password='123') + client.login(username='test', password='123') + return user \ No newline at end of file diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..c2851b1 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,51 @@ +import os + + +SECRET_KEY = 'fake-key-for-testing' +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'mfa' +] +ROOT_URLCONF="mfa.urls" + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } +} + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + os.path.join(BASE_DIR ,'mfa','templates' ), + os.path.join(BASE_DIR ,'tests','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', + ], + }, + }, +] + +MFA_UNALLOWED_METHODS = [] \ No newline at end of file diff --git a/tests/templates/base.html b/tests/templates/base.html new file mode 100644 index 0000000..0929c87 --- /dev/null +++ b/tests/templates/base.html @@ -0,0 +1,11 @@ + + + + {% block head %} + {% endblock %} + + + {% block content %} + {% endblock %} + + \ No newline at end of file diff --git a/tests/test_views.py b/tests/test_views.py index a1ace31..c39366a 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,2 +1,25 @@ import pytest +from django.urls import reverse + +@pytest.mark.django_db +def test_index_unauthenticated(client): + url = reverse("mfa_home") + response = client.get(url) + assert response is not None + assert response.status_code == 302 + assert response.url=="/accounts/login/?next=/" + + +@pytest.mark.django_db +def test_index_authenticated(client, authenticated_user): + url = reverse("mfa_home") + response = client.get(url) + assert response is not None + assert response.status_code == 200 + assert isinstance(response.templates, list) + assert len(response.templates) == 4 + for template in response.templates: + assert template.name in ["modal.html", "base.html", "mfa_base.html", "MFA.html"] + + From 0075f84b2977853babe98e0d461378b0381dcccf Mon Sep 17 00:00:00 2001 From: Oussama Jarrousse Date: Tue, 26 Dec 2023 17:36:26 +0100 Subject: [PATCH 3/7] added .vscode/launch.json --- .vscode/launch.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..650039b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "stopOnEntry": false, + "justMyCode": false, + "env": {"PYTEST_ADDOPTS": "--no-cov"}, + "purpose": ["debug-test"] + } + ] +} \ No newline at end of file From c53b4d1e1a2219375e813bbb9472df0739f64df8 Mon Sep 17 00:00:00 2001 From: Oussama Jarrousse Date: Tue, 26 Dec 2023 17:43:57 +0100 Subject: [PATCH 4/7] better .gitignore --- .gitignore | 9 ++++++++- .vscode/launch.json | 19 ------------------- 2 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index f3d5cc3..c22f190 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ -.idea example/venv + +# IDE +.idea +.vscode + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -105,3 +109,6 @@ venv.bak/ # mypy .mypy_cache/ example/test_db + +# OS related +.DS_Store diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 650039b..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal", - "stopOnEntry": false, - "justMyCode": false, - "env": {"PYTEST_ADDOPTS": "--no-cov"}, - "purpose": ["debug-test"] - } - ] -} \ No newline at end of file From 54f5eb212aad8121a16027975331c5a444158dec Mon Sep 17 00:00:00 2001 From: Oussama Jarrousse Date: Wed, 27 Dec 2023 11:36:25 +0100 Subject: [PATCH 5/7] added markers; edited README.md --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ pytest.ini | 3 +++ tests/test_views.py | 6 +++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2cae41f..d0c5002 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,44 @@ function some_func() { ```` +# Testing + +We use `pytest` and several pytest plugins, especially the `pytest-django` and `pytest-cov` plugins that provide Django fixtures, and test coverage analysis. + +In the root folder, `pytest.ini` contains configurations for running the tests, `requirements_testing.txt` contains the python packages required for running tests, and the folder `tests` contains the actual test files. + +To run the tests, install the packages in requirements and requirements_testing.txt: + +```bash +pip install -r requirements.txt +pip install -r requirements_testing.txt +``` + +then simply run pytest +``` +pytest +``` + +to generate the coverage html pages: + +``` +pytest --cov=. --cov-report html -v +``` + +the coverage html files will be generated in the `htmlcov` folder + +We use `tox` to test the package against different isolated environments. `tox.ini` contains the configurations for tox. To run the tests in the environments defined in the `tox.ini` file, make sure the python package tox is installed: + +``` +pip install tox +``` + +then run tox in the project root: + +``` +tox +``` + # Contributors * [mahmoodnasr](https://github.com/mahmoodnasr) * [d3cline](https://github.com/d3cline) @@ -207,6 +245,7 @@ function some_func() { * [ezrajrice](https://github.com/ezrajrice) * [Spitfireap](https://github.com/Spitfireap) * [peterthomassen](https://github.com/peterthomassen) +* [oussjarrousse](https://github.com/oussjarrousse) # Security contact information diff --git a/pytest.ini b/pytest.ini index e3e533a..f4a4976 100644 --- a/pytest.ini +++ b/pytest.ini @@ -95,6 +95,9 @@ markers = NON_PRIVILEGED_USER: tests for non-privileged users PERMISSIONS: tests related to permissions + ANNONYMOUS_USER: tests for non-authenticated users + AUTHENTICATED_USER: tests for authenticated users + ENDPOINTS: tests for endpoints (API nodes) SERIALIZERS: tests for serializers VIEWS: tests for DRF viewsets diff --git a/tests/test_views.py b/tests/test_views.py index c39366a..144946a 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,6 +1,9 @@ import pytest from django.urls import reverse +@pytest.mark.VIEWS +@pytest.mark.DJANGO +@pytest.mark.ANNONYMOUS_USER @pytest.mark.django_db def test_index_unauthenticated(client): url = reverse("mfa_home") @@ -9,7 +12,8 @@ def test_index_unauthenticated(client): assert response.status_code == 302 assert response.url=="/accounts/login/?next=/" - +@pytest.mark.VIEWS +@pytest.mark.AUTHENTICATED_USER @pytest.mark.django_db def test_index_authenticated(client, authenticated_user): url = reverse("mfa_home") From e0335ac4a7e32f1da6fd2c73694efea9a51c2d45 Mon Sep 17 00:00:00 2001 From: Oussama Jarrousse Date: Wed, 27 Dec 2023 11:54:23 +0100 Subject: [PATCH 6/7] added django3, 4, 5 to the tox environments list --- README.md | 4 ++-- tox.ini | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d0c5002..90072b7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Ema [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/django-mfa2.svg)](https://anaconda.org/conda-forge/django-mfa2) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/django-mfa2.svg)](https://anaconda.org/conda-forge/django-mfa2) + Web Authencation API (WebAuthn) is state-of-the art techology that is expected to replace passwords. ![Andriod Fingerprint](https://cdn-images-1.medium.com/max/800/1*1FWkRE8D7NTA2Kn1DrPjPA.png) @@ -205,8 +206,7 @@ In the root folder, `pytest.ini` contains configurations for running the tests, To run the tests, install the packages in requirements and requirements_testing.txt: ```bash -pip install -r requirements.txt -pip install -r requirements_testing.txt +pip install -r requirements.txt -r requirements_testing.txt ``` then simply run pytest diff --git a/tox.ini b/tox.ini index 23946b5..ab6f7cd 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,10 @@ # Choose your Python versions. They have to be available # on the system the tests are run on. # comma separated -envlist = py39, py310, py311 +envlist = + python{3.6,3.7,3.8,3.9}-django3 + python{3.8,3.9,3.10,3.11}-django4 + python{3.8,3.9,3.10,3.11}-django5 # Tell tox to not require a setup.py file ;skipsdist = True @@ -34,6 +37,7 @@ isolated_build = True deps = -rrequirements.txt + -rrequirements_testing.txt # https://tox.wiki/en/latest/example/basic.html#ignoring-a-command-exit-code commands = From e76feeb06b36e2ff8b83ac153ee88e6a93f84540 Mon Sep 17 00:00:00 2001 From: Oussama Jarrousse Date: Wed, 27 Dec 2023 12:08:49 +0100 Subject: [PATCH 7/7] excluding tests when building the package --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 19822e9..65e99ea 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( url = 'https://github.com/mkalioby/django-mfa2/', download_url='https://github.com/mkalioby/django-mfa2/', license='MIT', - packages=find_packages(), + packages=find_packages(exclude=("tests",)), install_requires=[ 'django >= 2.0', 'simplejson',