diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..56d0151 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,41 @@ +name: Publish package +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + architecture: 'x64' + + - name: Install dependencies and package + run: | + pip install -U pip + pip install -r requirements.txt + + - name: Build source and binary distribution package + run: | + python setup.py sdist bdist_wheel + env: + PACKAGE_VERSION: ${{ github.ref }} + + - name: Check distribution package + run: | + twine check dist/* + + - name: Publish distribution package + run: | + twine upload dist/* + env: + TWINE_REPOSITORY: ${{ secrets.PYPI_REPOSITORY }} + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + TWINE_NON_INTERACTIVE: yes diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..aecac77 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,56 @@ +name: Run linter and tests +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - '3.10' + - '3.11' + - '3.12' + - '3.13' + - 'pypy3.10' + django-version: + - '4.2' + - '5.0' + - '5.1' + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies and package + run: | + pip install -U pip + pip install -r requirements.txt + pip install django~=${{ matrix.django-version }} + + - name: Lint + run: | + pre-commit run --all-files + + - name: Run tests with coverage + run: | + # prepare Django project: link all necessary data from the test project into the root directory + # Hint: Simply changing the directory does not work (leads to missing files in coverage report) + ln -s ./tests/testapp testapp + ln -s ./tests/manage.py manage.py + ln -s ./tests/pytest.ini pytest.ini + + # run tests with coverage + pytest --cov=django_fernet --cov-report=xml testapp/tests/ + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + files: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + verbose: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd6f9f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.idea +.vscode +.env +.DS_Store +.pytest_cache +.coverage +coverage.xml +*.egg-info +*.pyc +*.pyo +db.sqlite3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e7c4126 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-merge-conflict + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: trailing-whitespace + args: ["--markdown-linebreak-ext=md"] + + - repo: https://github.com/asottile/pyupgrade + rev: v3.17.0 + hooks: + - id: pyupgrade + args: ["--py310-plus"] + + - repo: https://github.com/asottile/add-trailing-comma + rev: v3.1.0 + hooks: + - id: add-trailing-comma + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.2 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..43aa85e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Guidance on how to contribute + +> By submitting a pull request or filing a bug, issue, or feature request, +> you are agreeing to comply with this waiver of copyright interest. +> Details can be found in our [LICENSE](LICENSE). + + +There are two primary ways to help: + - Using the issue tracker, and + - Changing the code-base. + + +## Using the issue tracker + +Use the issue tracker to suggest feature requests, report bugs, and ask questions. +This is also a great way to connect with the developers of the project as well +as others who are interested in this solution. + +Use the issue tracker to find ways to contribute. Find a bug or a feature, mention in +the issue that you will take on that effort, then follow the _Changing the code-base_ +guidance below. + + +## Changing the code-base + +Generally speaking, you should fork this repository, make changes in your +own fork, and then submit a pull request. All new code should have associated +unit tests that validate implemented features and the presence or lack of defects. +Additionally, the code should follow any stylistic and architectural guidelines +prescribed by the project. In the absence of such guidelines, mimic the styles +and patterns in the existing code-base. + +### Contribution guidelines + - Your code should follow PEP 8 -- Style Guide for Python Code diff --git a/LICENSE b/LICENSE index 4b5777c..326ff64 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Anexia +Copyright (c) 2024 ANEXIA Internetdienstleisungs GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1ad8db8..bb4f182 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,139 @@ -# django-fernet -Django Model Fields that store the value encrypted with Fernet. +# Django Fernet + +[![PyPI](https://badge.fury.io/py/django-fernet.svg)](https://pypi.org/project/django-fernet/) +[![Test Status](https://github.com/anexia/django-fernet/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/anexia/django-fernet/actions/workflows/test.yml) +[![Codecov](https://codecov.io/gh/anexia/django-fernet/branch/main/graph/badge.svg)](https://codecov.io/gh/anexia/django-fernet) + +A library that provides Django model fields to store value encrypted using Fernet. + +## Installation + +With a [correctly configured](https://pipenv.pypa.io/en/latest/basics/#basic-usage-of-pipenv) `pipenv` toolchain: + +```bash +pipenv install django-fernet +``` + +You may also use classic `pip` to install the package: + +```bash +pip install django-fernet +``` + +### Auto-formatter setup +We use ruff (https://github.com/astral-sh/ruff) for local auto-formatting and for linting in the CI pipeline. +The pre-commit framework (https://pre-commit.com) provides Git hooks for these tools, so they are automatically applied +before every commit. + +Steps to activate: +* Install the pre-commit framework: `pip install pre-commit` (for alternative installation options + see https://pre-commit.com/#install) +* Activate the framework (from the root directory of the repository): `pre-commit install` + +Hint: You can also run the formatters manually at any time with the following command: `pre-commit run --all-files` + + +## Getting started + +### Example model that defines fernet text fields + +```python +from django.db import models +from django_fernet.fields import * + + +class ExampleTextModel(models.Model): + example_field = FernetTextField( + verbose_name="Example field", + ) +``` + +### Example model that defines fernet binary fields + +```python +from django.db import models +from django_fernet.fields import * + + +class ExampleBinaryModel(models.Model): + example_field = FernetBinaryField( + verbose_name="Example field", + ) +``` + + +## How to use + +### Save encrypted text to the database + +```python +from django_fernet.fernet import * + +field_data = FernetTextFieldData() +field_data.encrypt("foo", "--secret--") + +instance = ExampleTextModel() +instance.example_field = field_data +instance.save() +``` + +### Save encrypted binary data to the database + +```python +from django_fernet.fernet import * + +field_data = FernetTextFieldData() +field_data.encrypt(b"foo", "--secret--") + +instance = ExampleBinaryModel() +instance.example_field = field_data +instance.save() +``` + +### Load encrypted text from the database + +```python +instance = ExampleTextModel.objects.get(pk=...) +decrypted_str = instance.example_field.decrypt("--secret--") +``` + +### Load encrypted binary data from the database + +```python +instance = ExampleBinaryModel.objects.get(pk=...) +decrypted_bytes = instance.example_field.decrypt("--secret--") +``` + + +## Supported versions + +| | Django 4.2 | Django 5.0 | Django 5.1 | +|-------------|------------|------------|------------| +| Python 3.10 | ✓ | ✓ | ✓ | +| Python 3.11 | ✓ | ✓ | ✓ | +| Python 3.12 | ✓ | ✓ | ✓ | +| Python 3.13 | ✓ | ✓ | ✓ | +| PyPy 3.10 | ✓ | ✓ | ✓ | + + +## Tests + +An example Django app that makes use of `django-fernet` can be found in the [tests/](tests/) folder. This example +Django app also contains the unit tests. + +Follow below instructions to run the tests. You may exchange the installed Django version according to your +requirements. + +```bash +# install dependencies +python -m pip install --upgrade pip +pip install -r requirements.txt + +# run tests +cd tests && pytest +``` + + +## List of developers + +* Andreas Stocker diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5949fc2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Reporting Security Issues + +Please report any security issues you discovered to opensource[at]anexia-it[dot]com + +We will assess the risk, plus make a fix available before we create a GitHub issue. + +Thank you for your contribution. diff --git a/django_fernet/__init__.py b/django_fernet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_fernet/apps.py b/django_fernet/apps.py new file mode 100644 index 0000000..89f7d5b --- /dev/null +++ b/django_fernet/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + +__all__ = [ + "Config", +] + + +class Config(AppConfig): + name = "django_fernet" diff --git a/django_fernet/fernet.py b/django_fernet/fernet.py new file mode 100644 index 0000000..3f9b246 --- /dev/null +++ b/django_fernet/fernet.py @@ -0,0 +1,121 @@ +import base64 +import os + +from cryptography import fernet +from cryptography.hazmat import backends +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf import pbkdf2 + +__all__ = [ + "FernetBinaryFieldData", + "FernetTextFieldData", +] + + +class FernetBinaryFieldData: + ITERATIONS = 100000 + + def __init__(self, raw=None, base64_str=None): + if raw is not None and base64_str is not None: + raise ValueError("cannot set both 'raw' and 'base64_str'") + + if base64_str: + raw = base64.b64decode(base64_str.encode("ASCII")) + + if raw and len(raw) >= 16: + self._salt = raw[:16] + self._data = raw[16:] + + else: + self._salt = self._generate_salt() + self._data = None + + @property + def data(self): + if isinstance(self._data, str): + return self._data.encode("UTF-8") + elif self._data is not None: + return bytes(self._data) + else: + return None + + @data.setter + def data(self, value): + self._data = value + + @property + def salt(self): + if isinstance(self._salt, str): + return self._salt.encode("UTF-8") + elif self._salt is not None: + return bytes(self._salt) + else: + return None + + @salt.setter + def salt(self, value): + self._salt = value + + @property + def raw(self): + if self.data is None: + return None + else: + return bytes(self.salt) + bytes(self.data) + + @staticmethod + def _transform_before_encrypt(data): + return bytes(data) + + @staticmethod + def _transform_after_decrypt(data): + return bytes(data) + + @staticmethod + def _generate_salt(): + return os.urandom(16) + + def _get_fernet_key(self, secret): + secret = secret.encode("UTF-8") if isinstance(secret, str) else secret + + kdf = pbkdf2.PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=self.salt, + iterations=self.ITERATIONS, + backend=backends.default_backend(), + ) + key = base64.urlsafe_b64encode(kdf.derive(secret)) + + return key + + def encrypt(self, data, secret): + if data is None: + self.data = None + + else: + key = self._get_fernet_key(secret) + algo = fernet.Fernet(key) + self.data = algo.encrypt(self._transform_before_encrypt(data)) + + return self.data + + def decrypt(self, secret): + if self.data is None: + return None + + else: + key = self._get_fernet_key(secret) + algo = fernet.Fernet(key) + + return self._transform_after_decrypt(algo.decrypt(self.data)) + + +class FernetTextFieldData(FernetBinaryFieldData): + @staticmethod + def _transform_before_encrypt(data): + return data.encode("UTF-8") + + @staticmethod + def _transform_after_decrypt(data): + return data.decode("UTF-8") diff --git a/django_fernet/fields.py b/django_fernet/fields.py new file mode 100644 index 0000000..dd4558b --- /dev/null +++ b/django_fernet/fields.py @@ -0,0 +1,60 @@ +import base64 + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .fernet import * + +__all__ = [ + "FernetBinaryField", + "FernetTextField", +] + + +class FernetBinaryField(models.BinaryField): + description = _("Raw fernet encrypted binary data") + empty_values = [None, b""] + data_class = FernetBinaryFieldData + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def get_db_prep_value(self, value, connection, prepared=False): + if value in self.empty_values: + value = None + elif isinstance(value, self.data_class): + if value.data is None: + value = None + else: + value = value.raw + + return super().get_db_prep_value(value, connection, prepared) + + def from_db_value(self, value, expression, connection): + return self.to_python(value) + + def value_to_string(self, obj): + raw = self.value_from_object(obj).raw + + if raw is None: + return None + else: + return base64.b64encode(raw).decode("ASCII") + + def to_python(self, value): + if isinstance(value, self.data_class): + return value + + else: + value = super().to_python(value) + + if value in self.empty_values: + value = None + + return self.data_class(raw=value) + + +class FernetTextField(FernetBinaryField): + description = _("Fernet encrypted text data") + empty_values = [None, ""] + data_class = FernetTextFieldData diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c28f07f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel", +] +build-backend = "setuptools.build_meta" + +[tool.ruff] +line-length = 120 +respect-gitignore = true +extend-exclude = [".venv", "__pycache__"] + +[tool.ruff.format] +quote-style = "double" + +[tool.ruff.lint] +select = ["B", "C", "E", "F", "W", "B9"] +ignore = ["E203", "E266", "E501", "F403", "F401", "F405", "F901"] + +[tool.ruff.lint.isort] +known-first-party = ["django_fernet"] +section-order = [ + "future", + "standard-library", + "django", + "third-party", + "first-party", + "local-folder", +] + +[tool.ruff.lint.isort.sections] +"django" = ["django"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ff9053a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +# Package and package dependencies +-e . + +# Development dependencies +pre-commit>=4.0,<4.1 +pytest>=8.3,<8.4 +pytest-cov>=5.0.0 +pytest-django>=4.8,<4.9 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..495fe08 --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +import os + +from setuptools import find_packages, setup + +with open(os.path.join(os.path.dirname(__file__), "README.md")) as fh: + readme = fh.read() + +# allow setup.py to be run from any path +os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) + +setup( + name="django-fernet", + version=os.getenv("PACKAGE_VERSION", "0.0.0").replace("refs/tags/", ""), + packages=find_packages(), + include_package_data=True, + license="MIT", + description="Django model fields that store the values encrypted using Fernet.", + long_description=readme, + long_description_content_type="text/markdown", + url="https://github.com/anexia/django-fernet", + author="Andreas Stocker", + author_email="AStocker@anexia-it.com", + install_requires=[ + "django>=4.2", + "cryptography>=43.0", + ], + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Framework :: Django", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + ], +) diff --git a/tests/manage.py b/tests/manage.py new file mode 100755 index 0000000..090d917 --- /dev/null +++ b/tests/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?", + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..38a272f --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = --ds=testapp.settings +python_files = tests/test_*.py diff --git a/tests/testapp/__init__.py b/tests/testapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/testapp/asgi.py b/tests/testapp/asgi.py new file mode 100644 index 0000000..a4ee5be --- /dev/null +++ b/tests/testapp/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") + +application = get_asgi_application() diff --git a/tests/testapp/fixtures/test_binary.json b/tests/testapp/fixtures/test_binary.json new file mode 100644 index 0000000..eb4815b --- /dev/null +++ b/tests/testapp/fixtures/test_binary.json @@ -0,0 +1,10 @@ +[ +{ + "model": "testapp.fernetbinarymodel", + "pk": 1, + "fields": { + "nullable": "4+NJWeDrTbDG6TUyjXHCO2dBQUFBQUJuRzItLWNTSVBsT2Jfckp6V1h2amZXUmpVNkJNYm5iNU9RZHFtWXVNdDNiY1hGYUtXbWFNM0YzQnc0Vk53bDhkNHpPcXAtWm1Rb1hleVd3YTY4UjV5S245RG13PT0=", + "not_nullable": "DAGRX8P9w6ERwQcAkxkGKGdBQUFBQUJuRzJfSk50amJSVWhCMGQxaXVwbmFRN19qUmJid0hYTm1UU1ViV0VaWktjMlRud19uNl81b1NReHNJdllqalI1WnNHREVnMWpmZkd4TTA2V05tTjB4Y2tGS25BPT0=" + } +} +] diff --git a/tests/testapp/fixtures/test_text.json b/tests/testapp/fixtures/test_text.json new file mode 100644 index 0000000..d3aaad8 --- /dev/null +++ b/tests/testapp/fixtures/test_text.json @@ -0,0 +1,10 @@ +[ +{ + "model": "testapp.fernettextmodel", + "pk": 1, + "fields": { + "nullable": "AlJ2zZ5YOPVDTuVkmPZfzWdBQUFBQUJuRzIxT3RrMVJkZ2Z0ckk4QmlwQmVFdEZZUHdqbGdhdFQySHNycF9zMDBNZUtjT1RfTzMtVlY0SEFaZVE2VERzbnNYS2kwREwwVUQ0SlR6Nm1qZEdSdFRVNzV3PT0=", + "not_nullable": "zZvKDajYvv3ME+Lhujgz5mdBQUFBQUJuRzIxaHY3Q1E4QTNUTnpPYUtvNkhKMjlnOWUzMzQ2SkQ3cFdOdDY5LWZBWnJfSGZUeXhpOWgxb0JBZFZkQnhYb2Z1clVzSk83MmMtaU5EVjMza1NUaTRKVXZnPT0=" + } +} +] diff --git a/tests/testapp/migrations/0001_initial.py b/tests/testapp/migrations/0001_initial.py new file mode 100644 index 0000000..857b371 --- /dev/null +++ b/tests/testapp/migrations/0001_initial.py @@ -0,0 +1,67 @@ +import django_fernet.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="FernetBinaryModel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "nullable", + django_fernet.fields.FernetBinaryField( + blank=True, + null=True, + verbose_name="Nullable binary", + ), + ), + ( + "not_nullable", + django_fernet.fields.FernetBinaryField( + verbose_name="Not nullable binary", + ), + ), + ], + ), + migrations.CreateModel( + name="FernetTextModel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "nullable", + django_fernet.fields.FernetTextField( + blank=True, + null=True, + verbose_name="Nullable text", + ), + ), + ( + "not_nullable", + django_fernet.fields.FernetTextField( + verbose_name="Not nullable text", + ), + ), + ], + ), + ] diff --git a/tests/testapp/migrations/__init__.py b/tests/testapp/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/testapp/models.py b/tests/testapp/models.py new file mode 100644 index 0000000..d114d08 --- /dev/null +++ b/tests/testapp/models.py @@ -0,0 +1,34 @@ +from django.db import models + +from django_fernet import fields as fernet_fields + +__all__ = [ + "FernetTextModel", + "FernetBinaryModel", +] + + +class FernetTextModel(models.Model): + nullable = fernet_fields.FernetTextField( + verbose_name="Nullable text", + null=True, + blank=True, + ) + not_nullable = fernet_fields.FernetTextField( + verbose_name="Not nullable text", + null=False, + blank=False, + ) + + +class FernetBinaryModel(models.Model): + nullable = fernet_fields.FernetBinaryField( + verbose_name="Nullable binary", + null=True, + blank=True, + ) + not_nullable = fernet_fields.FernetBinaryField( + verbose_name="Not nullable binary", + null=False, + blank=False, + ) diff --git a/tests/testapp/settings.py b/tests/testapp/settings.py new file mode 100644 index 0000000..8d0d9e4 --- /dev/null +++ b/tests/testapp/settings.py @@ -0,0 +1,23 @@ +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent +SECRET_KEY = "django-insecure-r$ne8$0a$3und5&+#+e_ga(5q57#mud#4h1z-6092=0j(8(_@g" +DEBUG = True +ALLOWED_HOSTS = [] +INSTALLED_APPS = [ + "django_fernet", + "testapp", +] +MIDDLEWARE = [ + "django.middleware.common.CommonMiddleware", +] +ROOT_URLCONF = "testapp.urls" +WSGI_APPLICATION = "testapp.wsgi.application" +DATABASES = { + "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"}, +} +LANGUAGE_CODE = "de-at" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/tests/testapp/tests/__init__.py b/tests/testapp/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/testapp/tests/conftest.py b/tests/testapp/tests/conftest.py new file mode 100644 index 0000000..a41693f --- /dev/null +++ b/tests/testapp/tests/conftest.py @@ -0,0 +1,15 @@ +import pytest + +from django.core.management import call_command + + +@pytest.fixture() +def text_fixtures(django_db_setup, django_db_blocker): + with django_db_blocker.unblock(): + call_command("loaddata", "test_text.json") + + +@pytest.fixture() +def binary_fixtures(django_db_setup, django_db_blocker): + with django_db_blocker.unblock(): + call_command("loaddata", "test_binary.json") diff --git a/tests/testapp/tests/test_binary.py b/tests/testapp/tests/test_binary.py new file mode 100644 index 0000000..e29d9a3 --- /dev/null +++ b/tests/testapp/tests/test_binary.py @@ -0,0 +1,32 @@ +import pytest + +from django_fernet.fernet import * + +from testapp.models import * + + +@pytest.mark.django_db(transaction=True) +def test_simple_safe_and_load(): + field_value_1 = FernetBinaryFieldData() + field_value_1.encrypt(b"foo", "--secret--") + + field_value_2 = FernetBinaryFieldData() + field_value_2.encrypt(b"bar", "--secret--") + + instance = FernetBinaryModel() + instance.nullable = field_value_1 + instance.not_nullable = field_value_2 + instance.save() + + loaded_instance = FernetBinaryModel.objects.get(pk=instance.pk) + + assert loaded_instance.nullable.decrypt("--secret--") == b"foo" + assert loaded_instance.not_nullable.decrypt("--secret--") == b"bar" + + +@pytest.mark.django_db(transaction=True) +def test_load_fixture_data(binary_fixtures): + loaded_instance = FernetBinaryModel.objects.get(pk=1) + + assert loaded_instance.nullable.decrypt("--secret--") == b"fixture:foo" + assert loaded_instance.not_nullable.decrypt("--secret--") == b"fixture:bar" diff --git a/tests/testapp/tests/test_text.py b/tests/testapp/tests/test_text.py new file mode 100644 index 0000000..254039b --- /dev/null +++ b/tests/testapp/tests/test_text.py @@ -0,0 +1,32 @@ +import pytest + +from django_fernet.fernet import * + +from testapp.models import * + + +@pytest.mark.django_db(transaction=True) +def test_simple_safe_and_load(): + field_value_1 = FernetTextFieldData() + field_value_1.encrypt("foo", "--secret--") + + field_value_2 = FernetTextFieldData() + field_value_2.encrypt("bar", "--secret--") + + instance = FernetTextModel() + instance.nullable = field_value_1 + instance.not_nullable = field_value_2 + instance.save() + + loaded_instance = FernetTextModel.objects.get(pk=instance.pk) + + assert loaded_instance.nullable.decrypt("--secret--") == "foo" + assert loaded_instance.not_nullable.decrypt("--secret--") == "bar" + + +@pytest.mark.django_db(transaction=True) +def test_load_fixture_data(text_fixtures): + loaded_instance = FernetTextModel.objects.get(pk=1) + + assert loaded_instance.nullable.decrypt("--secret--") == "fixture:foo" + assert loaded_instance.not_nullable.decrypt("--secret--") == "fixture:bar" diff --git a/tests/testapp/urls.py b/tests/testapp/urls.py new file mode 100644 index 0000000..637600f --- /dev/null +++ b/tests/testapp/urls.py @@ -0,0 +1 @@ +urlpatterns = [] diff --git a/tests/testapp/wsgi.py b/tests/testapp/wsgi.py new file mode 100644 index 0000000..3cc955f --- /dev/null +++ b/tests/testapp/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") + +application = get_wsgi_application()