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/README.md b/README.md index 3e1cb1b..c0bed48 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) @@ -196,6 +197,43 @@ 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 -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) * [jkirkcaldy](https://github.com/jkirkcaldy) 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..f4a4976 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,109 @@ +[pytest] +# Searching +python_files = test_* +python_classes = Tests* +python_functions = test_* + +env_files = + .env + +# do not search for tests in these folders +norecursedirs = .vscode .tox docs example img mfa venv .coverage django_mfa2.egg-info + +# Add folder to PYTHONPATH +# requires pytest >= 7.0.0 +pythonpath = . + + +# https://pytest-django.readthedocs.io/en/latest/usage.html +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 +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=./mfa + # 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 + # 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 +# 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 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 + 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/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', 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..583e86d --- /dev/null +++ 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 new file mode 100644 index 0000000..144946a --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,29 @@ +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") + response = client.get(url) + assert response is not None + 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") + 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"] + + + diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ab6f7cd --- /dev/null +++ b/tox.ini @@ -0,0 +1,45 @@ +# 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 = + 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 + +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 + -rrequirements_testing.txt + +# https://tox.wiki/en/latest/example/basic.html#ignoring-a-command-exit-code +commands = +; pytest --junitxml=report.xml + pytest {posargs}