From a20d77e256450aea9dd76de27f43caa8f228730c Mon Sep 17 00:00:00 2001 From: Hendrik Schneider Date: Thu, 21 Apr 2022 14:15:59 +0200 Subject: [PATCH] setup github actions with black, flake8, testing and coverage --- .github/lint-and-test.yml | 53 ++++++++++++++++++++++++++++++ encrypted_fields/__init__.py | 2 +- encrypted_fields/fields.py | 40 +++++++++++++++-------- lint-and-test.yml | 53 ++++++++++++++++++++++++++++++ manage.py | 4 +-- package_test/settings.py | 18 ++++------- package_test/tests.py | 62 +++++++++++++++++------------------- setup.py | 26 +++++++-------- workflows/lint-and-test.yml | 53 ++++++++++++++++++++++++++++++ 9 files changed, 238 insertions(+), 73 deletions(-) create mode 100644 .github/lint-and-test.yml create mode 100644 lint-and-test.yml create mode 100644 workflows/lint-and-test.yml diff --git a/.github/lint-and-test.yml b/.github/lint-and-test.yml new file mode 100644 index 0000000..e3123ce --- /dev/null +++ b/.github/lint-and-test.yml @@ -0,0 +1,53 @@ +name: Lint & Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build: + strategy: + matrix: + django_version: [2.2, 3.0, 3.1, 3.2, 4.0a1] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -q Django==${{ matrix.django_version }} + pip install flake8 coverage black + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Lint with Black + run: | + black --check . + + - name: Run tests + run: | + coverage3 run --source='./encrypted_fields' manage.py test + coverage xml + + - name: "Upload coverage to Codecov" + uses: codecov/codecov-action@v2 + with: + fail_ci_if_error: true diff --git a/encrypted_fields/__init__.py b/encrypted_fields/__init__.py index b968739..7746f2c 100644 --- a/encrypted_fields/__init__.py +++ b/encrypted_fields/__init__.py @@ -1 +1 @@ -from .fields import * \ No newline at end of file +from .fields import * diff --git a/encrypted_fields/fields.py b/encrypted_fields/fields.py index c4d7b94..26e8b16 100644 --- a/encrypted_fields/fields.py +++ b/encrypted_fields/fields.py @@ -12,15 +12,25 @@ class EncryptedFieldMixin(object): @cached_property def keys(self): keys = [] - salt_keys = settings.SALT_KEY if isinstance(settings.SALT_KEY, list) else [settings.SALT_KEY] + salt_keys = ( + settings.SALT_KEY + if isinstance(settings.SALT_KEY, list) + else [settings.SALT_KEY] + ) for salt_key in salt_keys: - salt = bytes(salt_key, 'utf-8') - kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), - length=32, - salt=salt, - iterations=100000, - backend=default_backend()) - keys.append(base64.urlsafe_b64encode(kdf.derive(settings.SECRET_KEY.encode('utf-8')))) + salt = bytes(salt_key, "utf-8") + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + backend=default_backend(), + ) + keys.append( + base64.urlsafe_b64encode( + kdf.derive(settings.SECRET_KEY.encode("utf-8")) + ) + ) return keys @cached_property @@ -33,13 +43,13 @@ class EncryptedFieldMixin(object): """ To treat everything as text """ - return 'TextField' + return "TextField" def get_prep_value(self, value): if value: if not isinstance(value, str): value = str(value) - return self.f.encrypt(bytes(value, 'utf-8')).decode('utf-8') + return self.f.encrypt(bytes(value, "utf-8")).decode("utf-8") return None def get_db_prep_value(self, value, connection, prepared=False): @@ -51,14 +61,18 @@ class EncryptedFieldMixin(object): return self.to_python(value) def to_python(self, value): - if value is None or not isinstance(value, str) or hasattr(self, '_already_decrypted'): + if ( + value is None + or not isinstance(value, str) + or hasattr(self, "_already_decrypted") + ): return value - value = self.f.decrypt(bytes(value, 'utf-8')).decode('utf-8') + value = self.f.decrypt(bytes(value, "utf-8")).decode("utf-8") return super(EncryptedFieldMixin, self).to_python(value) def clean(self, value, model_instance): """ - Create and assign a semaphore so that to_python method will not try to decrypt an already decrypted value + Create and assign a semaphore so that to_python method will not try to decrypt an already decrypted value during cleaning of a form """ self._already_decrypted = True diff --git a/lint-and-test.yml b/lint-and-test.yml new file mode 100644 index 0000000..e3123ce --- /dev/null +++ b/lint-and-test.yml @@ -0,0 +1,53 @@ +name: Lint & Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build: + strategy: + matrix: + django_version: [2.2, 3.0, 3.1, 3.2, 4.0a1] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -q Django==${{ matrix.django_version }} + pip install flake8 coverage black + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Lint with Black + run: | + black --check . + + - name: Run tests + run: | + coverage3 run --source='./encrypted_fields' manage.py test + coverage xml + + - name: "Upload coverage to Codecov" + uses: codecov/codecov-action@v2 + with: + fail_ci_if_error: true diff --git a/manage.py b/manage.py index 8c4f5d0..dbe5fea 100644 --- a/manage.py +++ b/manage.py @@ -1,8 +1,8 @@ import os import sys -if __name__ == '__main__': - os.environ['DJANGO_SETTINGS_MODULE'] = 'package_test.settings' +if __name__ == "__main__": + os.environ["DJANGO_SETTINGS_MODULE"] = "package_test.settings" from django.core.management import execute_from_command_line diff --git a/package_test/settings.py b/package_test/settings.py index c0d28f4..e169bcb 100644 --- a/package_test/settings.py +++ b/package_test/settings.py @@ -1,18 +1,14 @@ DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", }, } -SECRET_KEY = 'abc' -SALT_KEY = 'xyz' +SECRET_KEY = "abc" +SALT_KEY = "xyz" -INSTALLED_APPS = ( - 'encrypted_fields', - 'package_test' -) +INSTALLED_APPS = ("encrypted_fields", "package_test") MIDDLEWARE_CLASSES = [] -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' - +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/package_test/tests.py b/package_test/tests.py index 68a4a5d..4372e4a 100644 --- a/package_test/tests.py +++ b/package_test/tests.py @@ -11,38 +11,38 @@ class FieldTest(TestCase): def get_db_value(self, field, model_id): cursor = connection.cursor() cursor.execute( - 'select {0} ' - 'from package_test_testmodel ' - 'where id = {1};'.format(field, model_id) + "select {0} " + "from package_test_testmodel " + "where id = {1};".format(field, model_id) ) return cursor.fetchone()[0] def test_char_field_encrypted(self): - plaintext = 'Oh hi, test reader!' + plaintext = "Oh hi, test reader!" model = TestModel() model.char = plaintext model.save() - ciphertext = self.get_db_value('char', model.id) + ciphertext = self.get_db_value("char", model.id) self.assertNotEqual(plaintext, ciphertext) - self.assertTrue('test' not in ciphertext) + self.assertTrue("test" not in ciphertext) fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.char, plaintext) def test_text_field_encrypted(self): - plaintext = 'Oh hi, test reader!' * 10 + plaintext = "Oh hi, test reader!" * 10 model = TestModel() model.text = plaintext model.save() - ciphertext = self.get_db_value('text', model.id) + ciphertext = self.get_db_value("text", model.id) self.assertNotEqual(plaintext, ciphertext) - self.assertTrue('test' not in ciphertext) + self.assertTrue("test" not in ciphertext) fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.text, plaintext) @@ -54,10 +54,10 @@ class FieldTest(TestCase): model.datetime = plaintext model.save() - ciphertext = self.get_db_value('datetime', model.id) + ciphertext = self.get_db_value("datetime", model.id) # Django's normal date serialization format - self.assertTrue(re.search('^\d\d\d\d-\d\d-\d\d', ciphertext) is None) + self.assertTrue(re.search("^\d\d\d\d-\d\d-\d\d", ciphertext) is None) fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.datetime, plaintext) @@ -69,7 +69,7 @@ class FieldTest(TestCase): model.integer = plaintext model.save() - ciphertext = self.get_db_value('integer', model.id) + ciphertext = self.get_db_value("integer", model.id) self.assertNotEqual(plaintext, ciphertext) self.assertNotEqual(plaintext, str(ciphertext)) @@ -84,7 +84,7 @@ class FieldTest(TestCase): model.date = plaintext model.save() - ciphertext = self.get_db_value('date', model.id) + ciphertext = self.get_db_value("date", model.id) fresh_model = TestModel.objects.get(id=model.id) self.assertNotEqual(ciphertext, plaintext.isoformat()) @@ -97,7 +97,7 @@ class FieldTest(TestCase): model.floating = plaintext model.save() - ciphertext = self.get_db_value('floating', model.id) + ciphertext = self.get_db_value("floating", model.id) self.assertNotEqual(plaintext, ciphertext) self.assertNotEqual(plaintext, str(ciphertext)) @@ -106,16 +106,16 @@ class FieldTest(TestCase): self.assertEqual(fresh_model.floating, plaintext) def test_email_field_encrypted(self): - plaintext = 'test@gmail.com' + plaintext = "test@gmail.com" model = TestModel() model.email = plaintext model.save() - ciphertext = self.get_db_value('email', model.id) + ciphertext = self.get_db_value("email", model.id) self.assertNotEqual(plaintext, ciphertext) - self.assertTrue('aron' not in ciphertext) + self.assertTrue("aron" not in ciphertext) fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.email, plaintext) @@ -127,13 +127,13 @@ class FieldTest(TestCase): model.boolean = plaintext model.save() - ciphertext = self.get_db_value('boolean', model.id) + ciphertext = self.get_db_value("boolean", model.id) self.assertNotEqual(plaintext, ciphertext) self.assertNotEqual(True, ciphertext) - self.assertNotEqual('True', ciphertext) - self.assertNotEqual('true', ciphertext) - self.assertNotEqual('1', ciphertext) + self.assertNotEqual("True", ciphertext) + self.assertNotEqual("true", ciphertext) + self.assertNotEqual("1", ciphertext) self.assertNotEqual(1, ciphertext) self.assertTrue(not isinstance(ciphertext, bool)) @@ -142,27 +142,24 @@ class FieldTest(TestCase): class RotatedSaltTestCase(TestCase): - @classmethod - @override_settings(SALT_KEY=['abcdefghijklmnopqrstuvwxyz0123456789']) + @override_settings(SALT_KEY=["abcdefghijklmnopqrstuvwxyz0123456789"]) def setUpTestData(cls): """Create the initial record using the old salt""" - cls.original = TestModel.objects.create( - text="Oh hi test reader" - ) + cls.original = TestModel.objects.create(text="Oh hi test reader") - @override_settings(SALT_KEY=['newkeyhere', 'abcdefghijklmnopqrstuvwxyz0123456789']) + @override_settings(SALT_KEY=["newkeyhere", "abcdefghijklmnopqrstuvwxyz0123456789"]) def test_rotated_salt(self): - """Chage the salt, keep the old one as the last in the list for reading""" + """Change the salt, keep the old one as the last in the list for reading""" plaintext = "Oh hi test reader" model = TestModel() model.text = plaintext model.save() - ciphertext = FieldTest.get_db_value(self, 'text', model.id) + ciphertext = FieldTest.get_db_value(self, "text", model.id) self.assertNotEqual(plaintext, ciphertext) - self.assertTrue('test' not in ciphertext) + self.assertTrue("test" not in ciphertext) fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.text, plaintext) @@ -170,5 +167,6 @@ class RotatedSaltTestCase(TestCase): old_record = TestModel.objects.get(id=self.original.id) self.assertEqual(fresh_model.text, old_record.text) - self.assertNotEqual(ciphertext, FieldTest.get_db_value(self, 'text', self.original.pk)) - + self.assertNotEqual( + ciphertext, FieldTest.get_db_value(self, "text", self.original.pk) + ) diff --git a/setup.py b/setup.py index 39bc24a..ab12ced 100644 --- a/setup.py +++ b/setup.py @@ -2,20 +2,18 @@ from __future__ import print_function from setuptools import setup setup( - name='django-fernet-encrypted-fields', - description=( - 'This is inspired by django-encrypted-fields.' - ), - long_description=open('README.md').read(), - long_description_content_type='text/markdown', - url='http://github.com/frgmt/django-fernet-encrypted-fields/', - license='MIT', - author='fragment.co.jp', - author_email='info@fragment.co.jp', - packages=['encrypted_fields'], - version='0.1.1', + name="django-fernet-encrypted-fields", + description=("This is inspired by django-encrypted-fields."), + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + url="http://github.com/frgmt/django-fernet-encrypted-fields/", + license="MIT", + author="fragment.co.jp", + author_email="info@fragment.co.jp", + packages=["encrypted_fields"], + version="0.1.1", install_requires=[ - 'Django>=2.2', - 'cryptography>=35.0.0', + "Django>=2.2", + "cryptography>=35.0.0", ], ) diff --git a/workflows/lint-and-test.yml b/workflows/lint-and-test.yml new file mode 100644 index 0000000..e3123ce --- /dev/null +++ b/workflows/lint-and-test.yml @@ -0,0 +1,53 @@ +name: Lint & Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build: + strategy: + matrix: + django_version: [2.2, 3.0, 3.1, 3.2, 4.0a1] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -q Django==${{ matrix.django_version }} + pip install flake8 coverage black + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Lint with Black + run: | + black --check . + + - name: Run tests + run: | + coverage3 run --source='./encrypted_fields' manage.py test + coverage xml + + - name: "Upload coverage to Codecov" + uses: codecov/codecov-action@v2 + with: + fail_ci_if_error: true