import decimal import json import uuid from io import StringIO from django import forms from django.conf import settings from django.core import exceptions, serializers, validators from django.core.management import call_command from django.db import IntegrityError, connection, models from django.test import TestCase, TransactionTestCase, override_settings from django.test.utils import freeze_time from django.utils import timezone from django_cryptography.fields import Expired, encrypt from .models import ( EncryptedCharModel, EncryptedDateTimeModel, EncryptedFieldSubclass, EncryptedIntegerModel, EncryptedNullableIntegerModel, EncryptedTTLIntegerModel, OtherEncryptedTypesModel, ) class TestSaveLoad(TestCase): def test_integer(self): instance = EncryptedIntegerModel(field=42) instance.save() loaded = EncryptedIntegerModel.objects.get() self.assertEqual(instance.field, loaded.field) def test_char(self): instance = EncryptedCharModel(field='Hello, world!') instance.save() loaded = EncryptedCharModel.objects.get() self.assertEqual(instance.field, loaded.field) def test_dates(self): instance = EncryptedDateTimeModel( datetime=timezone.now(), date=timezone.now().date(), time=timezone.now().time(), ) instance.save() loaded = EncryptedDateTimeModel.objects.get() self.assertEqual(instance.datetime, loaded.datetime) self.assertEqual(instance.date, loaded.date) self.assertEqual(instance.time, loaded.time) self.assertTrue(instance.auto_now) self.assertEqual(instance.auto_now, loaded.auto_now) def test_default_null(self): instance = EncryptedNullableIntegerModel() instance.save() loaded = EncryptedNullableIntegerModel.objects.get(pk=instance.pk) self.assertEqual(loaded.field, None) self.assertEqual(instance.field, loaded.field) def test_null_handling(self): instance = EncryptedNullableIntegerModel(field=None) instance.save() loaded = EncryptedNullableIntegerModel.objects.get() self.assertEqual(instance.field, loaded.field) instance = EncryptedIntegerModel(field=None) with self.assertRaises(IntegrityError): instance.save() def test_ttl(self): with freeze_time(499162800): instance = EncryptedTTLIntegerModel(field=42) instance.save() with freeze_time(123456789): loaded = EncryptedTTLIntegerModel.objects.get() self.assertIs(loaded.field, Expired) def test_other_types(self): instance = OtherEncryptedTypesModel( ip='192.168.0.1', uuid=uuid.uuid4(), decimal=decimal.Decimal(1.25), ) instance.save() loaded = OtherEncryptedTypesModel.objects.get() self.assertEqual(instance.ip, loaded.ip) self.assertEqual(instance.uuid, loaded.uuid) self.assertEqual(instance.decimal, loaded.decimal) def test_updates(self): with self.assertNumQueries(2): instance = EncryptedCharModel.objects.create(field='Hello, world!') instance.field = 'Goodbye, world!' instance.save() loaded = EncryptedCharModel.objects.get() self.assertEqual(instance.field, loaded.field) class TestQuerying(TestCase): def setUp(self): self.objs = [ EncryptedNullableIntegerModel.objects.create(field=1), EncryptedNullableIntegerModel.objects.create(field=2), EncryptedNullableIntegerModel.objects.create(field=3), EncryptedNullableIntegerModel.objects.create(field=None), ] def test_isnull(self): self.assertSequenceEqual( self.objs[-1:], EncryptedNullableIntegerModel.objects.filter(field__isnull=True)) def test_unsupported(self): with self.assertRaises(exceptions.FieldError): EncryptedNullableIntegerModel.objects.filter(field__exact=2) class TestChecks(TestCase): def test_settings_has_key(self): key = settings.CRYPTOGRAPHY_KEY self.assertIsNotNone(key) self.assertIsInstance(key, bytes) def test_field_description(self): field = encrypt(models.IntegerField()) self.assertEqual('Encrypted Integer', field.description) def test_field_checks(self): class BadField(models.Model): field = encrypt(models.CharField()) class Meta: app_label = 'myapp' model = BadField() errors = model.check() self.assertEqual(len(errors), 1) # The inner CharField is missing a max_length. self.assertEqual('fields.E120', errors[0].id) self.assertIn('max_length', errors[0].msg) def test_invalid_base_fields(self): class Related(models.Model): field = encrypt( models.ForeignKey('fields.EncryptedIntegerModel', models.CASCADE)) class Meta: app_label = 'myapp' obj = Related() errors = obj.check() self.assertEqual(1, len(errors)) self.assertEqual('encrypted.E002', errors[0].id) class TestMigrations(TransactionTestCase): available_apps = ['tests.fields'] def test_clone(self): field = encrypt(models.IntegerField()) new_field = field.clone() self.assertIsNot(field, new_field) self.assertEqual(field.verbose_name, new_field.verbose_name) self.assertNotEqual(field.creation_counter, new_field.creation_counter) def test_subclass_clone(self): field = EncryptedFieldSubclass() new_field = field.clone() self.assertIsNot(field, new_field) self.assertEqual(field.verbose_name, new_field.verbose_name) self.assertNotEqual(field.creation_counter, new_field.creation_counter) def test_deconstruct(self): field = encrypt(models.IntegerField()) name, path, args, kwargs = field.deconstruct() new = encrypt(*args, **kwargs) self.assertEqual(type(new), type(field)) def test_deconstruct_with_ttl(self): field = encrypt(models.IntegerField(), ttl=60) name, path, args, kwargs = field.deconstruct() new = encrypt(*args, **kwargs) self.assertEqual(new.ttl, field.ttl) def test_deconstruct_args(self): field = encrypt(models.CharField(max_length=20)) name, path, args, kwargs = field.deconstruct() new = encrypt(*args, **kwargs) self.assertEqual(new.max_length, field.max_length) def test_subclass_deconstruct(self): field = encrypt(models.IntegerField()) name, path, args, kwargs = field.deconstruct() self.assertEqual('django_cryptography.fields.encrypt', path) field = EncryptedFieldSubclass() name, path, args, kwargs = field.deconstruct() self.assertEqual('tests.fields.models.EncryptedFieldSubclass', path) @override_settings(MIGRATION_MODULES={ 'fields': 'tests.fields.test_migrations_encrypted_default' }) def test_adding_field_with_default(self): table_name = 'fields_integerencrypteddefaultmodel' with connection.cursor() as cursor: self.assertNotIn(table_name, connection.introspection.table_names(cursor)) call_command('migrate', 'fields', verbosity=0) with connection.cursor() as cursor: self.assertIn(table_name, connection.introspection.table_names(cursor)) call_command('migrate', 'fields', 'zero', verbosity=0) with connection.cursor() as cursor: self.assertNotIn(table_name, connection.introspection.table_names(cursor)) @override_settings(MIGRATION_MODULES={ 'fields': 'tests.fields.test_migrations_normal_to_encrypted' }) def test_makemigrations_no_changes(self): out = StringIO() call_command('makemigrations', '--dry-run', 'fields', stdout=out) self.assertIn("No changes detected in app 'fields'", out.getvalue()) class TestSerialization(TestCase): test_data_integer = ( '[{"fields": {"field": 42}, "model": "fields.encryptedintegermodel", "pk": null}]' ) test_data_char = ( '[{"fields": {"field": "Hello, world!"}, "model": "fields.encryptedcharmodel", "pk": null}]' ) def test_integer_dumping(self): instance = EncryptedIntegerModel(field=42) data = serializers.serialize('json', [instance]) self.assertEqual(json.loads(self.test_data_integer), json.loads(data)) def test_integer_loading(self): instance = list( serializers.deserialize('json', self.test_data_integer))[0].object self.assertEqual(42, instance.field) def test_char_dumping(self): instance = EncryptedCharModel(field='Hello, world!') data = serializers.serialize('json', [instance]) self.assertEqual(json.loads(self.test_data_char), json.loads(data)) def test_char_loading(self): instance = list(serializers.deserialize('json', self.test_data_char))[0].object self.assertEqual('Hello, world!', instance.field) class TestValidation(TestCase): def test_unbounded(self): field = encrypt(models.IntegerField()) with self.assertRaises(exceptions.ValidationError) as cm: field.clean(None, None) self.assertEqual('null', cm.exception.code) self.assertEqual('This field cannot be null.', cm.exception.messages[0]) def test_blank_true(self): field = encrypt(models.IntegerField(blank=True, null=True)) # This should not raise a validation error field.clean(None, None) def test_with_validators(self): field = encrypt( models.IntegerField(validators=[validators.MinValueValidator(1)])) field.clean(1, None) with self.assertRaises(exceptions.ValidationError) as cm: field.clean(0, None) self.assertEqual('Ensure this value is greater than or equal to 1.', cm.exception.messages[0]) class TestFormField(TestCase): class EncryptedCharModelForm(forms.ModelForm): class Meta: fields = '__all__' model = EncryptedCharModel def test_model_field_formfield(self): model_field = encrypt(models.CharField(max_length=27)) form_field = model_field.formfield() self.assertIsInstance(form_field, forms.CharField) self.assertEqual(form_field.max_length, 27) def test_model_form(self): data = {'field': 'Hello, world!'} form = self.EncryptedCharModelForm(data) self.assertTrue(form.is_valid(), form.errors) self.assertEqual({'field': 'Hello, world!'}, form.cleaned_data) instance = form.save() loaded = EncryptedCharModel.objects.get() self.assertEqual(instance.field, loaded.field) def test_model_form_update(self): data = {'field': 'Goodbye, world!'} instance = EncryptedCharModel.objects.create(field='Hello, world!') form = self.EncryptedCharModelForm(data, instance=instance) self.assertTrue(form.is_valid(), form.errors) form.save() loaded = EncryptedCharModel.objects.get() self.assertEqual(data['field'], loaded.field)