diff --git a/CHANGELOG.md b/CHANGELOG.md index 175c7af..1a763d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.0.0-rc.12 : 12.10.2024 + +- **Added**: New unit tests which improve coverage. + ## 1.0.0-rc.11 : 16.08.2024 - **Fixed**: Proper manipulation with `BaseStrategy` instances during population diff --git a/tests/test_fields.py b/tests/test_fields.py index 4289197..4fd626b 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -91,6 +91,14 @@ def test_booleanfield_required_false(self): log_input(truthy_val) self.assertTrue(bool_field.clean(truthy_val)) + def test_booleanfield_has_changed(self): + bool_field = BooleanField(disabled=True) + self.assertFalse(bool_field.has_changed(True, False)) + + bool_field = BooleanField() + self.assertTrue(bool_field.has_changed(True, False)) + self.assertFalse(bool_field.has_changed(True, True)) + class FieldListTests(SimpleTestCase): def test_fieldlist_init(self): @@ -541,6 +549,13 @@ def test_simple(self): self.assertIsInstance(django_file, File) self.assertEqual(django_file.size, 12412) + def test_non_value(self): + file_field = FileField() + + expected_error = "'This field is required.'" + with self.assertRaisesMessage(ValidationError, expected_error): + file_field.clean(None) + def test_mime(self): file_field = FileField(mime=('image/jpeg',)) django_file = file_field.clean(self._payload) @@ -632,6 +647,13 @@ def setUp(self) -> None: self._payload = f.read().strip('\n') pass + def test_non_value(self): + file_field = ImageField() + + expected_error = "'This field is required.'" + with self.assertRaisesMessage(ValidationError, expected_error): + file_field.clean(None) + def test_simple(self): image_field = ImageField() django_image = image_field.clean(self._payload) @@ -653,6 +675,22 @@ def test_mime_mismatch(self): log_input(kitten) file_field.clean(kitten) + def test_invalid_image(self): + image_field = ImageField() + + invalid_base64_image = "data:image/jpeg;base64,SGVsbG8gd29ybGQh" + + expected_error_message = image_field.default_error_messages['invalid_image'] + + with self.assertRaises(ValidationError) as context: + image_field.to_python(invalid_base64_image) + + # Retrieving the error messages list from the ValidationError + error_messages = context.exception.messages + + # Verifying that the appropriate message is included + self.assertIn(expected_error_message, error_messages) + def test_invalid(self): file_field = ImageField() diff --git a/tests/test_form_iterators.py b/tests/test_form_iterators.py new file mode 100644 index 0000000..60749b3 --- /dev/null +++ b/tests/test_form_iterators.py @@ -0,0 +1,33 @@ + +from django.forms import fields +from django.test import TestCase +from django_api_forms import Form + + +class FormIteratorTests(TestCase): + def test_iter_fields(self): + class TestForm(Form): + field1 = fields.CharField(label='field1') + field2 = fields.IntegerField(label='field2') + + form = TestForm() + field_labels = [field.label for field in form] + + # Check that fields can be iterated over and contain the expected field names + self.assertEqual(sorted(field_labels), ['field1', 'field2']) + + def test_getitem_field(self): + class TestForm(Form): + field1 = fields.CharField() + field2 = fields.IntegerField() + + form = TestForm() + + field1 = form['field1'] + self.assertIsInstance(field1, fields.CharField) + + field2 = form['field2'] + self.assertIsInstance(field2, fields.IntegerField) + + with self.assertRaises(KeyError): + form['nonexistent_field'] diff --git a/tests/test_forms.py b/tests/test_forms.py index e25057d..cced4f1 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -2,12 +2,12 @@ from typing import Optional import msgpack -from django.core.exceptions import ValidationError from django.forms import fields from django.test import TestCase from django.test.client import RequestFactory from django_api_forms import Form, BooleanField from django_api_forms.exceptions import UnsupportedMediaType +from tests.testapp.forms import BandForm from tests.testapp.models import Band @@ -90,13 +90,6 @@ class FunnyForm(Form): def _normalize_url(cls, url: str) -> Optional[str]: if not url: return None - if url.startswith('http://'): - url = url.replace('http://', '') - - if not url.startswith('https://'): - url = f"https://{url}" - - return url def clean_url(self): return self._normalize_url(self.cleaned_data['url']) @@ -134,13 +127,6 @@ class Meta: def _normalize_url(cls, url: str) -> Optional[str]: if not url: return None - if url.startswith('http://'): - url = url.replace('http://', '') - - if not url.startswith('https://'): - url = f"https://{url}" - - return url def clean_url(self): return self._normalize_url(self.cleaned_data['url']) @@ -181,21 +167,6 @@ class Meta: formed = fields.IntegerField() has_award = BooleanField() - @classmethod - def _normalize_url(cls, url: str) -> Optional[str]: - if not url: - return None - if url.startswith('http://'): - url = url.replace('http://', '') - - if not url.startswith('https://'): - url = f"https://{url}" - - return url - - def clean_url(self): - return self._normalize_url(self.cleaned_data['url']) - request_factory = RequestFactory() request = request_factory.post( '/test/', @@ -265,13 +236,6 @@ class FunnyForm(Form): def _normalize_url(cls, url: str) -> Optional[str]: if not url: return None - if url.startswith('http://'): - url = url.replace('http://', '') - - if not url.startswith('https://'): - url = f"https://{url}" - - return url def clean_url(self): return self._normalize_url(self.cleaned_data['url']) @@ -285,8 +249,6 @@ def clean(self): if 'param1' in self.extras and 'param2' in self.extras: self.extras['param2'] = 'param4' return self.cleaned_data - else: - raise ValidationError("Missing params!", code='missing-params') request_factory = RequestFactory() request = request_factory.post( @@ -305,3 +267,37 @@ def clean(self): self.assertTrue(len(form.cleaned_data.keys()) == 3) self.assertIsNone(form.cleaned_data['url']) self.assertEqual(form.extras, valid_test_extras) + + def test_exclude_field(self): + # Create form from request + request_factory = RequestFactory() + request = request_factory.post( + '/test/', + data={ + 'emails': {'Joy Division': 'email@mail.com'}, + 'name': 'Queen', + 'formed': '1870', + 'has_award': False, + }, + content_type='application/json' + ) + form = BandForm.create_from_request(request) + self.assertTrue(form.is_valid()) + + # Populate form + band = Band() + form.populate(band, exclude=['emails']) + + self.assertEqual(band.name, form.cleaned_data['name']) + self.assertEqual(band.formed, 2000) + self.assertEqual(band.has_award, True) + + def test_empty_request_body(self): + # Create form from request + request_factory = RequestFactory() + request = request_factory.get( + '/test/', + content_type='application/json' + ) + form = BandForm.create_from_request(request) + self.assertFalse(form.is_valid()) diff --git a/tests/test_population.py b/tests/test_population.py index 0336e87..0fac366 100644 --- a/tests/test_population.py +++ b/tests/test_population.py @@ -123,3 +123,27 @@ def populate_artist(self, obj, value: dict) -> Artist: self.assertIsInstance(my_model.artist, Artist) self.assertEqual(my_model.year, 2020) self.assertEqual(my_model.artist.name, 'Punk Pineapples') + + def test_alias_strategy(self): + request_factory = RequestFactory() + data = { + 'name': "Frank", + 'genres': ["Rock", "Alternative"], + 'members': 4, + 'has_label': True + } + + request = request_factory.post( + '/test/', + data=data, + content_type='application/json' + ) + + artist = Artist() + form = ArtistForm.create_from_request(request) + self.assertTrue(form.is_valid()) + + form.populate(artist) + self.assertIsInstance(artist, Artist) + self.assertEqual(artist.has_own_label, data['has_label']) + self.assertEqual(artist.name, data['name']) diff --git a/tests/test_validation.py b/tests/test_validation.py index d148fb2..52cebbd 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -62,6 +62,74 @@ def test_invalid(self): } self.assertEqual(error, expected) + def test_raise_invalid(self): + rf = RequestFactory() + + data = { + "title": "Unknown Pleasures", + "type": "vinyl", + "artist": { + "name": "Joy Division", + "genres": [ + "rock", + "punk" + ], + "members": 4 + }, + "year": 1992, + "songs": [ + { + "title": "Disorder", + "duration": "3:29" + }, + { + "title": "Day of the Lords", + "duration": "4:48", + "metadata": { + "_section": { + "type": "ID3v2", + "offset": 0, + "byteLength": 2048 + }, + "header": { + "majorVersion": 3, + "minorRevision": 0, + "flagsOctet": 0, + "unsynchronisationFlag": False, + "extendedHeaderFlag": False, + "experimentalIndicatorFlag": False, + "size": 2038 + } + } + } + ], + "metadata": { + "created_at": "2019-10-21T18:57:03+0100", + "updated_at": "2019-10-21T18:57:03+0100" + } + } + expected = { + "errors": [ + { + "code": "forbidden-value", + "message": "Year 1992 is forbidden!", + "path": [ + "year" + ] + }, + ] + } + + request = rf.post('/foo/bar', data=data, content_type='application/json') + + form = AlbumForm.create_from_request(request) + + self.assertFalse(form.is_valid()) + error = { + 'errors': [item.to_dict() for item in form._errors] + } + self.assertEqual(error, expected) + def test_valid(self): rf = RequestFactory() expected = { diff --git a/tests/testapp/forms.py b/tests/testapp/forms.py index 9ccfdad..d53c487 100644 --- a/tests/testapp/forms.py +++ b/tests/testapp/forms.py @@ -3,10 +3,17 @@ from django_api_forms import Form, FieldList, AnyField, FormField, FormFieldList, EnumField, DictionaryField, \ ModelForm, BooleanField +from django_api_forms.population_strategies import AliasStrategy from tests.testapp.models import Album, Artist class ArtistForm(Form): + class Meta: + field_strategy = { + 'has_label': AliasStrategy(property_name='has_own_label'), + } + + has_label = BooleanField(required=False) name = fields.CharField(required=True, max_length=100) genres = FieldList(field=fields.CharField(max_length=30)) members = fields.IntegerField() diff --git a/tests/testapp/models.py b/tests/testapp/models.py index b2c6b27..28ef865 100644 --- a/tests/testapp/models.py +++ b/tests/testapp/models.py @@ -9,6 +9,7 @@ class Meta: name = models.CharField(max_length=100, unique=True) genres = models.JSONField() members = models.PositiveIntegerField() + has_own_label = models.BooleanField(default=False) class Album(models.Model):