diff --git a/README.md b/README.md index 7529058..6400b33 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,10 @@ DRF-EXTRA-FIELDS Extra Fields for Django Rest Framework -**Possible breaking change in v3.1.0**: In this version we have changed file class used in `Base64FileField` from `ContentFile` to `SimpleUploadedFile` (you may see the change [here](https://github.com/Hipo/drf-extra-fields/pull/149/files#diff-5f77bcb61083cd9c026f6dfb3b77bf8fa824c45e620cdb7826ad713bde7b65f8L72-R85)). +**Possible breaking change in v3.1.0**: In this version we have changed file class used in `Base64FileField` +from `ContentFile` to `SimpleUploadedFile` (you may see the +change [here](https://github.com/Hipo/drf-extra-fields/pull/149/files#diff-5f77bcb61083cd9c026f6dfb3b77bf8fa824c45e620cdb7826ad713bde7b65f8L72-R85)) +. [![Build Status](https://travis-ci.org/Hipo/drf-extra-fields.svg?branch=master)](https://travis-ci.org/Hipo/drf-extra-fields) [![codecov](https://codecov.io/gh/Hipo/drf-extra-fields/branch/master/graph/badge.svg)](https://codecov.io/gh/Hipo/drf-extra-fields) @@ -20,6 +23,7 @@ pip install drf-extra-fields ``` **Note:** + - **This package renamed as "drf-extra-fields", earlier it was named as django-extra-fields.** - Install version 0.1 for Django Rest Framework 2.* - Install version 0.3 or greater for Django Rest Framework 3.* @@ -27,22 +31,22 @@ pip install drf-extra-fields Fields: ---------------- - ## Base64ImageField An image representation for Base64ImageField Inherited from `ImageField` - **Signature:** `Base64ImageField()` - - It takes a base64 image as a string. - - A base64 image: `data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7` - - Base64ImageField accepts the entire string or just the part after base64, `R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7` - - It takes the optional parameter `represent_in_base64` (`False` by default), if set to `True` it will allow for base64-encoded downloads of an `ImageField`. - - You can inherit the `Base64ImageField` class and set allowed extensions (`ALLOWED_TYPES` list), or customize the validation messages (`INVALID_FILE_MESSAGE`, `INVALID_TYPE_MESSAGE`) - +- It takes a base64 image as a string. +- A base64 image: `data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7` +- Base64ImageField accepts the entire string or just the part after + base64, `R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7` +- It takes the optional parameter `represent_in_base64` (`False` by default), if set to `True` it will allow for + base64-encoded downloads of an `ImageField`. +- You can inherit the `Base64ImageField` class and set allowed extensions (`ALLOWED_TYPES` list), or customize the + validation messages (`INVALID_FILE_MESSAGE`, `INVALID_TYPE_MESSAGE`) **Example:** @@ -51,29 +55,29 @@ Inherited from `ImageField` from drf_extra_fields.fields import Base64ImageField + class UploadedBase64ImageSerializer(serializers.Serializer): file = Base64ImageField(required=False) created = serializers.DateTimeField() + # use the serializer file = 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' serializer = UploadedBase64ImageSerializer(data={'created': now, 'file': file}) ``` - ## Base64FileField A file representation for Base64FileField Inherited from `FileField` - **Signature:** `Base64FileField()` - - It takes a base64 file as a string. - - Other options like for Base64ImageField - - You have to provide your own full implementation of this class. You have to implement file validation in `get_file_extension` method and set `ALLOWED_TYPES` list. - +- It takes a base64 file as a string. +- Other options like for Base64ImageField +- You have to provide your own full implementation of this class. You have to implement file validation + in `get_file_extension` method and set `ALLOWED_TYPES` list. **Example:** @@ -90,23 +94,21 @@ class PDFBase64File(Base64FileField): return 'pdf' ``` - ## PointField Point field for GeoDjango - **Signature:** `PointField()` - - It takes a dictionary contains latitude and longitude keys like below +- It takes a dictionary contains latitude and longitude keys like below - { - "latitude": 49.8782482189424, - "longitude": 24.452545489 - } - - It takes the optional parameter `str_points` (False by default), if set to True it serializes the longitude/latitude - values as strings - - It takes the optional parameter `srid` (None by default), if set the Point created object will have its srid attribute set to the same value. + { + "latitude": 49.8782482189424, + "longitude": 24.452545489 } +- It takes the optional parameter `str_points` (False by default), if set to True it serializes the longitude/latitude + values as strings +- It takes the optional parameter `srid` (None by default), if set the Point created object will have its srid attribute + set to the same value. **Example:** @@ -115,26 +117,29 @@ Point field for GeoDjango from drf_extra_fields.geo_fields import PointField + class PointFieldSerializer(serializers.Serializer): point = PointField(required=False) created = serializers.DateTimeField() + # use the serializer point = { "latitude": 49.8782482189424, "longitude": 24.452545489 - } +} serializer = PointFieldSerializer(data={'created': now, 'point': point}) ``` - # RangeField -The Range Fields map to Django's PostgreSQL specific [Range Fields](https://docs.djangoproject.com/en/stable/ref/contrib/postgres/fields/#range-fields). +The Range Fields map to Django's PostgreSQL +specific [Range Fields](https://docs.djangoproject.com/en/stable/ref/contrib/postgres/fields/#range-fields). Each accepts an optional parameter `child_attrs`, which allows passing parameters to the child field. -For example, calling `IntegerRangeField(child_attrs={"allow_null": True})` allows deserializing data with a null value for `lower` and/or `upper`: +For example, calling `IntegerRangeField(child_attrs={"allow_null": True})` allows deserializing data with a null value +for `lower` and/or `upper`: ```python from rest_framework import serializers @@ -224,7 +229,8 @@ class RangeSerializer(serializers.Serializer): ranges = DateTimeRangeField() -serializer = RangeSerializer(data={'ranges': {'lower': datetime.datetime(2015, 1, 1, 0), 'upper': datetime.datetime(2015, 2, 1, 0)}}) +serializer = RangeSerializer( + data={'ranges': {'lower': datetime.datetime(2015, 1, 1, 0), 'upper': datetime.datetime(2015, 2, 1, 0)}}) ``` @@ -232,11 +238,13 @@ serializer = RangeSerializer(data={'ranges': {'lower': datetime.datetime(2015, 1 Represents related object with a serializer. -`presentation_serializer` could also be a string that represents a dotted path of a serializer, this is useful when you want to represent a related field with the same serializer. +`presentation_serializer` could also be a string that represents a dotted path of a serializer, this is useful when you +want to represent a related field with the same serializer. ```python from drf_extra_fields.relations import PresentablePrimaryKeyRelatedField + class UserSerializer(serializers.ModelSerializer): class Meta: model = User @@ -245,6 +253,7 @@ class UserSerializer(serializers.ModelSerializer): "username", ) + class PostSerializer(serializers.ModelSerializer): user = PresentablePrimaryKeyRelatedField( queryset=User.objects.all(), @@ -260,6 +269,7 @@ class PostSerializer(serializers.ModelSerializer): }, read_source=None ) + class Meta: model = Post fields = ( @@ -270,6 +280,7 @@ class PostSerializer(serializers.ModelSerializer): ``` **Serializer data:** + ``` { "user": 1, @@ -278,6 +289,7 @@ class PostSerializer(serializers.ModelSerializer): ``` **Serialized data with PrimaryKeyRelatedField:** + ``` { "id":1, @@ -287,6 +299,7 @@ class PostSerializer(serializers.ModelSerializer): ``` **Serialized data with PresentablePrimaryKeyRelatedField:** + ``` { "id":1, @@ -298,7 +311,6 @@ class PostSerializer(serializers.ModelSerializer): } ``` - ## PresentableSlugRelatedField Represents related object retrieved using slug with a serializer. @@ -306,6 +318,7 @@ Represents related object retrieved using slug with a serializer. ```python from drf_extra_fields.relations import PresentableSlugRelatedField + class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category @@ -315,6 +328,7 @@ class CategorySerializer(serializers.ModelSerializer): "name" ) + class ProductSerializer(serializers.ModelSerializer): category = PresentableSlugRelatedField( slug_field="slug", @@ -331,6 +345,7 @@ class ProductSerializer(serializers.ModelSerializer): }, read_source=None ) + class Meta: model = Product fields = ( @@ -341,6 +356,7 @@ class ProductSerializer(serializers.ModelSerializer): ``` **Serializer data:** + ``` { "category": "vegetables", @@ -349,6 +365,7 @@ class ProductSerializer(serializers.ModelSerializer): ``` **Serialized data with SlugRelatedField:** + ``` { "id": 1, @@ -358,6 +375,7 @@ class ProductSerializer(serializers.ModelSerializer): ``` **Serialized data with PresentableSlugRelatedField:** + ``` { "id": 1, @@ -371,9 +389,12 @@ class ProductSerializer(serializers.ModelSerializer): ``` ### read_source parameter -This parameter allows you to use different `source` for read operations and doesn't change field name for write operations. This is only used while representing the data. + +This parameter allows you to use different `source` for read operations and doesn't change field name for write +operations. This is only used while representing the data. ## HybridImageField + A django-rest-framework field for handling image-uploads through raw post data, with a fallback to multipart form data. It first tries Base64ImageField. if it fails then tries ImageField. @@ -389,7 +410,9 @@ class HybridImageSerializer(serializers.Serializer): drf-yasg fix for BASE64 Fields: ---------------- -The [drf-yasg](https://github.com/axnsan12/drf-yasg) project seems to generate wrong documentation on Base64ImageField or Base64FileField. It marks those fields as readonly. Here is the workaround code for correct the generated document. (More detail on issue [#66](https://github.com/Hipo/drf-extra-fields/issues/66)) +The [drf-yasg](https://github.com/axnsan12/drf-yasg) project seems to generate wrong documentation on Base64ImageField +or Base64FileField. It marks those fields as readonly. Here is the workaround code for correct the generated document. ( +More detail on issue [#66](https://github.com/Hipo/drf-extra-fields/issues/66)) ```python class PDFBase64FileField(Base64FileField): @@ -412,9 +435,10 @@ class PDFBase64FileField(Base64FileField): return 'pdf' ``` - ## LowercaseEmailField -An enhancement over django-rest-framework's EmailField to allow case-insensitive serialization and deserialization of e-mail addresses. + +An enhancement over django-rest-framework's EmailField to allow case-insensitive serialization and deserialization of +e-mail addresses. ```python from rest_framework import serializers @@ -426,12 +450,46 @@ class EmailSerializer(serializers.Serializer): ``` +## CryptoBinaryField and CryptoCharField + ++ These are django-rest-framework fields for handling encryption through serialisation. Inputs are `String` objects and internal +python representation is `Binary` object for `CryptoBinaryField` and `String` object for `CryptoCharField` + ++ They take the optional parameter `salt` (Django's `SECRET_KEY` imported from setting as default). If set the value will be used as the cryptographic salt. ++ They take the optional parameter `password` (`"Non_nobis1solum?nati!sumus"` as default). If set the value will be used as the password for encryption. **It is highly recommended to use custom one!!** ++ They take the optional parameter `ttl` (`None` as default). If set the value will be used to manage the number of seconds that a message is valid. If the message is older than `ttl` seconds (from the time it was originally created) the field will return `None` and the encrypted message will not be able to be decrypted. + + **Example** + +```python +from rest_framework import serializers +from drf_extra_fields.crypto_fields import CryptoCharField + + +class CryptoSerializer(serializers.Serializer): + crypto_char = CryptoCharField() + +``` + **Example with parameters** ++ It takes custom `salt` and `password` parameters. Once saved it will be available for 1000 seconds, after that it won't be decrypted and will return `None` . + +```python +from rest_framework import serializers +from drf_extra_fields.crypto_fields import CryptoCharField + + +class CryptoSerializer(serializers.Serializer): + crypto_char = CryptoCharField(salt="custom salt", password="custom password", ttl=1000) + +``` + CONTRIBUTION ================= **TESTS** -- Make sure that you add the test for contributed field to test/test_fields.py -and run with command before sending a pull request: + +- Make sure that you add the test for contributed field to test/test_fields.py and run with command before sending a + pull request: ```bash $ pip install tox # if not already installed @@ -447,22 +505,19 @@ tox ``` **README** -- Make sure that you add the documentation for the field added to README.md +- Make sure that you add the documentation for the field added to README.md LICENSE ==================== Copyright DRF EXTRA FIELDS HIPO -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " +AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. diff --git a/drf_extra_fields/crypto_fields.py b/drf_extra_fields/crypto_fields.py new file mode 100644 index 0000000..fe62b3c --- /dev/null +++ b/drf_extra_fields/crypto_fields.py @@ -0,0 +1,140 @@ +import base64 + +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers +import time + +EMPTY_VALUES = (None, "", [], (), {}) +DEFAULT_PASSWORD = b"Non_nobis1solum?nati!sumus" +DEFAULT_SALT = settings.SECRET_KEY + + +def _generate_password_key(salt=DEFAULT_SALT, password=DEFAULT_PASSWORD): + key_derivation_function = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=_to_bytes(salt), + iterations=100000, + ) + + key = base64.urlsafe_b64encode(kdf.derive(_to_bytes(password))) + return key + + +def _to_bytes(v): + if isinstance(v, str): + return v.encode("utf-8") + + if isinstance(v, bytes): + return v + + raise TypeError( + _( + "SALT & PASSWORD must be specified as strings that convert nicely to " + "bytes." + ) + ) + + +def _encrypt(token, value_in_str): + b_message = value_in_str.encode("utf-8") + encrypted_message = token.encrypt(b_message) + return encrypted_message + + +def _decrypt(token, value, ttl=None): + ttl = int(ttl) if ttl else None + decrypted_message = token.decrypt(_to_bytes(value), ttl) + return decrypted_message.decode("utf-8") + + +def _get_timestamp(token, value): + timestamp = token.extract_timestamp(_to_bytes(value)) + return int(timestamp) + + +class CryptoBinaryField(serializers.Field): + """ + A django-rest-framework field for handling encryption through serialisation, where inputs are string + and internal python representations are Binary objects. + """ + + type_name = "CryptoBinaryField" + type_label = "crypto" + + default_error_messages = { + "invalid": _("Input a valid data"), + } + + def __init__(self, *args, **kwargs): + self.salt = kwargs.pop("salt", DEFAULT_SALT) + self.password = kwargs.pop("password", DEFAULT_PASSWORD) + self.ttl = kwargs.pop("ttl", None) + super(CryptoBinaryField, self).__init__(*args, **kwargs) + + def to_internal_value(self, value): + """ + Parse input data to encrypted binary data + """ + if value in EMPTY_VALUES and not self.required: + return None + + if isinstance(value, str): + key = _generate_password_key(self.salt, self.password) + token = Fernet(key) + encrypted_message = _encrypt(token, value) + return encrypted_message + + self.fail("invalid") + + def to_representation(self, value): + """ + Transform encrypted data to decrypted string. + """ + if value is None: + return value + if isinstance(value, str): + value = value.encode("utf-8") + elif isinstance(value, (bytearray, memoryview)): + value = bytes(value) + if isinstance(value, bytes): + key = _generate_password_key(self.salt, self.password) + token = Fernet(key) + try: + decrypted_message = _decrypt(token, value, self.ttl) + return decrypted_message + except InvalidToken: + + if self.ttl is not None: + # timestamp, data = Fernet._get_unverified_token_data(token) + # timestamp = Fernet.extract_timestamp(token) + # timestamp = token.extract_timestamp(token) + timestamp = _get_timestamp(token, value) + current_time = int(time.time()) + if timestamp + self.ttl < current_time: + raise InvalidToken(_("The Token ttl has expired")) + + raise InvalidToken(_("Valid Token could not be created")) + + self.fail("invalid") + + +class CryptoCharField(CryptoBinaryField): + """ + A django-rest-framework field for handling encryption through serialisation, where input are string + and internal python representation is String object. + """ + + type_name = "CryptoBinaryField" + + def to_internal_value(self, value): + value = super(CryptoCharField, self).to_internal_value(value) + if value is None: + return value + elif value: + return value.decode("utf-8") + self.fail("invalid") diff --git a/drf_extra_fields/runtests/settings.py b/drf_extra_fields/runtests/settings.py index b815550..a89783d 100644 --- a/drf_extra_fields/runtests/settings.py +++ b/drf_extra_fields/runtests/settings.py @@ -107,7 +107,8 @@ # 'rest_framework.tests.users', ) -STATIC_URL = '/static/' + +STATIC_URL = "/static/" PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.SHA1PasswordHasher', diff --git a/requirements_dev.txt b/requirements_dev.txt index 88bee08..59a21cb 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,5 @@ Pillow >= 6.2.1 +cryptography >= 3.2.1 pytest-django pytest-cov psycopg2-binary diff --git a/setup.py b/setup.py index 03676cd..5fa6c15 100644 --- a/setup.py +++ b/setup.py @@ -1,47 +1,49 @@ import os from setuptools import setup -with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: +with open(os.path.join(os.path.dirname(__file__), "README.md")) as readme: README = readme.read() -with open(os.path.join(os.path.dirname(__file__), 'requirements.txt')) as requirements_txt: +with open( + os.path.join(os.path.dirname(__file__), "requirements.txt") +) as requirements_txt: requirements = requirements_txt.read().strip().splitlines() # 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='drf-extra-fields', - version='3.1.1', - packages=['drf_extra_fields', - 'drf_extra_fields.runtests'], + name="drf-extra-fields", + version="3.1.1", + packages=["drf_extra_fields", "drf_extra_fields.runtests"], include_package_data=True, extras_require={ "Base64ImageField": ["Pillow >= 6.2.1"], + "CryptoBinaryField": ["cryptography >= 3.2.1"], }, - license='Apache-2.0', - license_files=['LICENSE'], - description='Additional fields for Django Rest Framework.', + license="Apache-2.0", + license_files=["LICENSE"], + description="Additional fields for Django Rest Framework.", long_description=README, long_description_content_type="text/markdown", - author='hipo', - author_email='pypi@hipolabs.com', - url='https://github.com/Hipo/drf-extra-fields', + author="hipo", + author_email="pypi@hipolabs.com", + url="https://github.com/Hipo/drf-extra-fields", python_requires=">=3.5", install_requires=requirements, classifiers=[ - 'Environment :: Web Environment', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ], ) diff --git a/tests/models.py b/tests/models.py new file mode 100644 index 0000000..0f6547b --- /dev/null +++ b/tests/models.py @@ -0,0 +1,38 @@ +# import datetime +from django.db import models +# import pytz + +from drf_extra_fields.crypto_field import ( + # CryptoFieldMixin, + CryptoTextField, + CryptoCharField, + CryptoEmailField, + CryptoIntegerField, + CryptoDateField, + CryptoDateTimeField, + CryptoBigIntegerField, + CryptoPositiveIntegerField, + CryptoPositiveSmallIntegerField, + CryptoSmallIntegerField, +) + +class TextModel(models.Model): + text_field = CryptoTextField() + +class CharModel(models.Model): + char_field = CryptoCharField() + +# class DemoModel(models.Model): +# password = "Password123!" +# text_field = 'RandomText123!' +# char_field = 'RandomChar123!' +# email_field = 'random@email.com' +# integer_field = 123 +# date_field = datetime.date(2001, 1, 1) +# datetime_field = datetime.datetime(2001, 1, 1, 13, 00, tzinfo=pytz.utc) +# big_integer_field = -9223372036854775808 +# positive_integer_field = 9223372036854775808 +# positive_small_integer_field = 1 +# small_integer_field = -1 +# + diff --git a/tests/test_crypto_fields.py b/tests/test_crypto_fields.py new file mode 100644 index 0000000..5bc4d08 --- /dev/null +++ b/tests/test_crypto_fields.py @@ -0,0 +1,132 @@ +import time + +from rest_framework import serializers +from drf_extra_fields.crypto_fields import ( + CryptoBinaryField, + CryptoCharField, + _generate_password_key, + _encrypt, +) +import datetime +from django.test import TestCase +from django.conf import settings +from cryptography.fernet import Fernet, InvalidToken +from django.utils.translation import gettext_lazy as _ + +DEFAULT_PASSWORD = b"Non_nobis1solum?nati!sumus" +DEFAULT_SALT = settings.SECRET_KEY + + +class Message(object): + def __init__(self, message=None, created=None): + self.message = message + self.created = created or datetime.datetime.now() + + +class CryptoSerializer(serializers.Serializer): + message = CryptoBinaryField(required=False) + created = serializers.DateTimeField() + + def update(self, instance, validated_data): + instance.message = validated_data["message"] + return instance + + def create(self, validated_data): + return SaveCrypto(**validated_data) + + +class CryptoCharSerializer(serializers.Serializer): + message = CryptoCharField(required=False) + created = serializers.DateTimeField() + + +class SaltCryptoSerializerSerializer(CryptoSerializer): + message = CryptoBinaryField(salt="Salt") + created = serializers.DateTimeField() + + +class PasswordCryptoSerializerSerializer(CryptoSerializer): + message = CryptoBinaryField(password="Password") + created = serializers.DateTimeField() + + +class TtlCryptoSerializerSerializer(CryptoSerializer): + message = CryptoBinaryField(ttl=1) + created = serializers.DateTimeField() + + +class CryptoFieldsTest(TestCase): + def test_create(self): + """ + Test for creating CryptoBinaryField + """ + now = datetime.datetime.now() + message = "test message" + serializer = CryptoSerializer(data={"created": now, "message": message}) + model_data = SaveCrypto(message=message, created=now) + + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.validated_data["created"], model_data.created) + self.assertFalse(serializer.validated_data is model_data) + self.assertIs(type(serializer.validated_data["message"]), bytes) + + def test_create_char(self): + """ + Test for creating CryptoCharField + """ + now = datetime.datetime.now() + message = "test message" + serializer = CryptoCharSerializer(data={"created": now, "message": message}) + model_data = SaveCrypto(message=message, created=now) + + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.validated_data["created"], model_data.created) + self.assertFalse(serializer.validated_data is model_data) + self.assertIs(type(serializer.validated_data["message"]), str) + + def test_serialization(self): + """ + Regular JSON serialization should output float values + """ + now = datetime.datetime.now() + message = "test message" + key = _generate_password_key(DEFAULT_SALT, DEFAULT_PASSWORD) + token = Fernet(key) + encrypted_message = _encrypt(token, message) + model_data = SaveCrypto(message=encrypted_message, created=now) + serializer = CryptoSerializer(model_data) + self.assertEqual(serializer.data["message"], message) + + def test_serialization_salt(self): + now = datetime.datetime.now() + message = "test message" + key = _generate_password_key("Salt", DEFAULT_PASSWORD) + token = Fernet(key) + encrypted_message = _encrypt(token, message) + model_data = SaveCrypto(message=encrypted_message, created=now) + serializer = SaltCryptoSerializerSerializer(model_data) + time.sleep(3) + self.assertEqual(serializer.data["message"], message) + + def test_serialization_password(self): + now = datetime.datetime.now() + message = "test message" + key = _generate_password_key(DEFAULT_SALT, "Password") + token = Fernet(key) + encrypted_message = _encrypt(token, message) + model_data = SaveCrypto(message=encrypted_message, created=now) + serializer = PasswordCryptoSerializerSerializer(model_data) + time.sleep(3) + self.assertEqual(serializer.data["message"], message) + + def test_serialization_ttl(self): + now = datetime.datetime.now() + message = "test message" + key = _generate_password_key(DEFAULT_SALT, DEFAULT_PASSWORD) + token = Fernet(key) + encrypted_message = _encrypt(token, message) + model_data = SaveCrypto(message=encrypted_message, created=now) + serializer = TtlCryptoSerializerSerializer(model_data) + time.sleep(3) + with self.assertRaises(InvalidToken): + return serializer.data["message"] diff --git a/tox.ini b/tox.ini index c9a9549..1e883fb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] envlist = flake8, - py{35,36,37}-drf3-django{22}, - py{36,37,38}-drf3-django{30,31} + py{35,36,37}-drf3-django{22}-crt35, + py{36,37,38}-drf3-django{30,31}-crt36 [testenv] deps = @@ -10,7 +10,10 @@ deps = django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 drf3: djangorestframework>=3 + crt35: cryptography == 3.2.1 + crt36: cryptography == 3.3.1 -r requirements_dev.txt + commands = py.test {posargs} --cov-report=xml --cov passenv =