From 368182414ed1bb0958cbd4aed4b8b20f0c17c2ce Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Sun, 9 Feb 2025 19:59:19 +0530 Subject: [PATCH 1/4] Duplicate error msg fix for IP address field --- rest_framework/utils/field_mapping.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index fc63f96fe0..e04199a5d7 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -207,8 +207,10 @@ def get_field_kwargs(field_name, model_field): if isinstance(model_field, models.GenericIPAddressField): validator_kwarg = [ validator for validator in validator_kwarg - if validator is not validators.validate_ipv46_address + if validator not in [validators.validate_ipv46_address, validators.validate_ipv6_address, validators.validate_ipv4_address] ] + kwargs['protocol'] = getattr(model_field, 'protocol', 'both') + # Our decimal validation is handled in the field code, not validator code. if isinstance(model_field, models.DecimalField): validator_kwarg = [ From 08aba54700b113fc91dbaebb6215703da8d50fa4 Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Mon, 10 Feb 2025 23:48:01 +0530 Subject: [PATCH 2/4] WIP --- tests/test_ipaddress_field_serializer.py | 97 ++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/test_ipaddress_field_serializer.py diff --git a/tests/test_ipaddress_field_serializer.py b/tests/test_ipaddress_field_serializer.py new file mode 100644 index 0000000000..8cc0268453 --- /dev/null +++ b/tests/test_ipaddress_field_serializer.py @@ -0,0 +1,97 @@ +from django.db import models +from django.test import TestCase + +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + + +# Define the model +class TestModel(models.Model): + address = models.GenericIPAddressField(protocol="both") + + class Meta: + app_label = "main" + + +class TestSerializer(serializers.ModelSerializer): + + class Meta: + model = TestModel + fields = "__all__" + + +# Define the serializer in setUp +class TestSerializerTestCase(TestCase): + def setUp(self): + """Initialize serializer class.""" + self.serializer_class = TestSerializer + + def test_invalid_ipv4_for_ipv4_field(self): + """Test that an invalid IPv4 raises only an IPv4-related error.""" + TestModel._meta.get_field("address").protocol = "IPv4" # Set field to IPv4 only + invalid_data = {"address": "invalid-ip"} + serializer = self.serializer_class(data=invalid_data) + + with self.assertRaises(ValidationError) as context: + serializer.is_valid(raise_exception=True) + + self.assertEqual( + str(context.exception.detail["address"][0]), + "Enter a valid IPv4 address." + ) + + def test_invalid_ipv6_for_ipv6_field(self): + """Test that an invalid IPv6 raises only an IPv6-related error.""" + TestModel._meta.get_field("address").protocol = "IPv6" # Set field to IPv6 only + invalid_data = {"address": "invalid-ip"} + serializer = self.serializer_class(data=invalid_data) + + with self.assertRaises(ValidationError) as context: + serializer.is_valid(raise_exception=True) + + self.assertEqual( + str(context.exception.detail["address"][0]), + "Enter a valid IPv6 address." + ) + + def test_invalid_both_protocol(self): + """Test that an invalid IP raises a combined error message when protocol is both.""" + TestModel._meta.get_field("address").protocol = "both" # Allow both IPv4 & IPv6 + invalid_data = {"address": "invalid-ip"} + serializer = self.serializer_class(data=invalid_data) + + with self.assertRaises(ValidationError) as context: + serializer.is_valid(raise_exception=True) + + self.assertEqual( + str(context.exception.detail["address"][0]), + "Enter a valid IPv4 or IPv6 address." + ) + + def test_valid_ipv4(self): + """Test that a valid IPv4 passes validation.""" + TestModel._meta.get_field("address").protocol = "IPv4" + valid_data = {"address": "192.168.1.1"} + serializer = self.serializer_class(data=valid_data) + self.assertTrue(serializer.is_valid()) + + def test_valid_ipv6(self): + """Test that a valid IPv6 passes validation.""" + TestModel._meta.get_field("address").protocol = "IPv6" + valid_data = {"address": "2001:db8::ff00:42:8329"} + serializer = self.serializer_class(data=valid_data) + self.assertTrue(serializer.is_valid()) + + def test_valid_ipv4_for_both_protocol(self): + """Test that a valid IPv4 is accepted when protocol is 'both'.""" + TestModel._meta.get_field("address").protocol = "both" + valid_data = {"address": "192.168.1.1"} + serializer = self.serializer_class(data=valid_data) + self.assertTrue(serializer.is_valid()) + + def test_valid_ipv6_for_both_protocol(self): + """Test that a valid IPv6 is accepted when protocol is 'both'.""" + TestModel._meta.get_field("address").protocol = "both" + valid_data = {"address": "2001:db8::ff00:42:8329"} + serializer = self.serializer_class(data=valid_data) + self.assertTrue(serializer.is_valid()) From ec927c9083fddccb58825d9523c3b5d71946434e Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Tue, 11 Feb 2025 22:24:43 +0530 Subject: [PATCH 3/4] Refactored Test cases for GenericIPAddress Field --- tests/test_ipaddress_field_serializer.py | 97 ------------------------ tests/test_model_serializer.py | 80 ++++++++++++++++++- 2 files changed, 78 insertions(+), 99 deletions(-) delete mode 100644 tests/test_ipaddress_field_serializer.py diff --git a/tests/test_ipaddress_field_serializer.py b/tests/test_ipaddress_field_serializer.py deleted file mode 100644 index 8cc0268453..0000000000 --- a/tests/test_ipaddress_field_serializer.py +++ /dev/null @@ -1,97 +0,0 @@ -from django.db import models -from django.test import TestCase - -from rest_framework import serializers -from rest_framework.exceptions import ValidationError - - -# Define the model -class TestModel(models.Model): - address = models.GenericIPAddressField(protocol="both") - - class Meta: - app_label = "main" - - -class TestSerializer(serializers.ModelSerializer): - - class Meta: - model = TestModel - fields = "__all__" - - -# Define the serializer in setUp -class TestSerializerTestCase(TestCase): - def setUp(self): - """Initialize serializer class.""" - self.serializer_class = TestSerializer - - def test_invalid_ipv4_for_ipv4_field(self): - """Test that an invalid IPv4 raises only an IPv4-related error.""" - TestModel._meta.get_field("address").protocol = "IPv4" # Set field to IPv4 only - invalid_data = {"address": "invalid-ip"} - serializer = self.serializer_class(data=invalid_data) - - with self.assertRaises(ValidationError) as context: - serializer.is_valid(raise_exception=True) - - self.assertEqual( - str(context.exception.detail["address"][0]), - "Enter a valid IPv4 address." - ) - - def test_invalid_ipv6_for_ipv6_field(self): - """Test that an invalid IPv6 raises only an IPv6-related error.""" - TestModel._meta.get_field("address").protocol = "IPv6" # Set field to IPv6 only - invalid_data = {"address": "invalid-ip"} - serializer = self.serializer_class(data=invalid_data) - - with self.assertRaises(ValidationError) as context: - serializer.is_valid(raise_exception=True) - - self.assertEqual( - str(context.exception.detail["address"][0]), - "Enter a valid IPv6 address." - ) - - def test_invalid_both_protocol(self): - """Test that an invalid IP raises a combined error message when protocol is both.""" - TestModel._meta.get_field("address").protocol = "both" # Allow both IPv4 & IPv6 - invalid_data = {"address": "invalid-ip"} - serializer = self.serializer_class(data=invalid_data) - - with self.assertRaises(ValidationError) as context: - serializer.is_valid(raise_exception=True) - - self.assertEqual( - str(context.exception.detail["address"][0]), - "Enter a valid IPv4 or IPv6 address." - ) - - def test_valid_ipv4(self): - """Test that a valid IPv4 passes validation.""" - TestModel._meta.get_field("address").protocol = "IPv4" - valid_data = {"address": "192.168.1.1"} - serializer = self.serializer_class(data=valid_data) - self.assertTrue(serializer.is_valid()) - - def test_valid_ipv6(self): - """Test that a valid IPv6 passes validation.""" - TestModel._meta.get_field("address").protocol = "IPv6" - valid_data = {"address": "2001:db8::ff00:42:8329"} - serializer = self.serializer_class(data=valid_data) - self.assertTrue(serializer.is_valid()) - - def test_valid_ipv4_for_both_protocol(self): - """Test that a valid IPv4 is accepted when protocol is 'both'.""" - TestModel._meta.get_field("address").protocol = "both" - valid_data = {"address": "192.168.1.1"} - serializer = self.serializer_class(data=valid_data) - self.assertTrue(serializer.is_valid()) - - def test_valid_ipv6_for_both_protocol(self): - """Test that a valid IPv6 is accepted when protocol is 'both'.""" - TestModel._meta.get_field("address").protocol = "both" - valid_data = {"address": "2001:db8::ff00:42:8329"} - serializer = self.serializer_class(data=valid_data) - self.assertTrue(serializer.is_valid()) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index ae1a2b0fa1..95459196c3 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -25,6 +25,7 @@ from rest_framework import serializers from rest_framework.compat import postgres_fields +from rest_framework.exceptions import ValidationError from .models import NestedForeignKeySource @@ -409,7 +410,8 @@ class Meta: class TestGenericIPAddressFieldValidation(TestCase): - def test_ip_address_validation(self): + + def setUp(self): class IPAddressFieldModel(models.Model): address = models.GenericIPAddressField() @@ -418,12 +420,86 @@ class Meta: model = IPAddressFieldModel fields = '__all__' - s = TestSerializer(data={'address': 'not an ip address'}) + self.serializer_class = TestSerializer + self.model = IPAddressFieldModel + + def test_ip_address_validation(self): + s = self.serializer_class(data={'address': 'not an ip address'}) self.assertFalse(s.is_valid()) self.assertEqual(1, len(s.errors['address']), 'Unexpected number of validation errors: ' '{}'.format(s.errors)) + def test_invalid_ipv4_for_ipv4_field(self): + """Test that an invalid IPv4 raises only an IPv4-related error.""" + self.model._meta.get_field("address").protocol = "IPv4" # Set field to IPv4 only + invalid_data = {"address": "invalid-ip"} + serializer = self.serializer_class(data=invalid_data) + + with self.assertRaises(ValidationError) as context: + serializer.is_valid(raise_exception=True) + + self.assertEqual( + str(context.exception.detail["address"][0]), + "Enter a valid IPv4 address." + ) + + def test_invalid_ipv6_for_ipv6_field(self): + """Test that an invalid IPv6 raises only an IPv6-related error.""" + self.model._meta.get_field("address").protocol = "IPv6" # Set field to IPv6 only + invalid_data = {"address": "invalid-ip"} + serializer = self.serializer_class(data=invalid_data) + + with self.assertRaises(ValidationError) as context: + serializer.is_valid(raise_exception=True) + + self.assertEqual( + str(context.exception.detail["address"][0]), + "Enter a valid IPv6 address." + ) + + def test_invalid_both_protocol(self): + """Test that an invalid IP raises a combined error message when protocol is both.""" + self.model._meta.get_field("address").protocol = "both" # Allow both IPv4 & IPv6 + invalid_data = {"address": "invalid-ip"} + serializer = self.serializer_class(data=invalid_data) + + with self.assertRaises(ValidationError) as context: + serializer.is_valid(raise_exception=True) + + self.assertEqual( + str(context.exception.detail["address"][0]), + "Enter a valid IPv4 or IPv6 address." + ) + + def test_valid_ipv4(self): + """Test that a valid IPv4 passes validation.""" + self.model._meta.get_field("address").protocol = "IPv4" + valid_data = {"address": "192.168.1.1"} + serializer = self.serializer_class(data=valid_data) + self.assertTrue(serializer.is_valid()) + + def test_valid_ipv6(self): + """Test that a valid IPv6 passes validation.""" + self.model._meta.get_field("address").protocol = "IPv6" + valid_data = {"address": "2001:db8::ff00:42:8329"} + serializer = self.serializer_class(data=valid_data) + self.assertTrue(serializer.is_valid()) + + def test_valid_ipv4_for_both_protocol(self): + """Test that a valid IPv4 is accepted when protocol is 'both'.""" + self.model._meta.get_field("address").protocol = "both" + valid_data = {"address": "192.168.1.1"} + serializer = self.serializer_class(data=valid_data) + self.assertTrue(serializer.is_valid()) + + def test_valid_ipv6_for_both_protocol(self): + """Test that a valid IPv6 is accepted when protocol is 'both'.""" + self.model._meta.get_field("address").protocol = "both" + valid_data = {"address": "2001:db8::ff00:42:8329"} + serializer = self.serializer_class(data=valid_data) + self.assertTrue(serializer.is_valid()) + @pytest.mark.skipif('not postgres_fields') class TestPosgresFieldsMapping(TestCase): From 6128db6c1d11e2cc707d307e1ce5858468ab02a0 Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Fri, 14 Feb 2025 23:35:28 +0530 Subject: [PATCH 4/4] Refactored Test Cases with additional models and Serializers --- tests/test_model_serializer.py | 81 +++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 95459196c3..31723704d3 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -412,19 +412,39 @@ class Meta: class TestGenericIPAddressFieldValidation(TestCase): def setUp(self): - class IPAddressFieldModel(models.Model): - address = models.GenericIPAddressField() + class IPv4Model(models.Model): + address = models.GenericIPAddressField(protocol="IPv4") - class TestSerializer(serializers.ModelSerializer): + class IPv4TestSerializer(serializers.ModelSerializer): + class Meta: + model = IPv4Model + fields = '__all__' + + class IPv6Model(models.Model): + address = models.GenericIPAddressField(protocol="IPv6") + + class IPv6TestSerializer(serializers.ModelSerializer): + class Meta: + model = IPv6Model + fields = '__all__' + + class BothProtocolsModel(models.Model): + address = models.GenericIPAddressField(protocol="both") + + class BothProtocolsTestSerializer(serializers.ModelSerializer): class Meta: - model = IPAddressFieldModel + model = BothProtocolsModel fields = '__all__' - self.serializer_class = TestSerializer - self.model = IPAddressFieldModel + self.ipv4_serializer = IPv4TestSerializer + self.ipv4_model = IPv4Model + self.ipv6_serializer = IPv6TestSerializer + self.ipv6_model = IPv6Model + self.both_protocols_serializer = BothProtocolsTestSerializer + self.both_protocols_model = BothProtocolsModel def test_ip_address_validation(self): - s = self.serializer_class(data={'address': 'not an ip address'}) + s = self.both_protocols_serializer(data={'address': 'not an ip address'}) self.assertFalse(s.is_valid()) self.assertEqual(1, len(s.errors['address']), 'Unexpected number of validation errors: ' @@ -432,9 +452,8 @@ def test_ip_address_validation(self): def test_invalid_ipv4_for_ipv4_field(self): """Test that an invalid IPv4 raises only an IPv4-related error.""" - self.model._meta.get_field("address").protocol = "IPv4" # Set field to IPv4 only invalid_data = {"address": "invalid-ip"} - serializer = self.serializer_class(data=invalid_data) + serializer = self.ipv4_serializer(data=invalid_data) with self.assertRaises(ValidationError) as context: serializer.is_valid(raise_exception=True) @@ -446,9 +465,34 @@ def test_invalid_ipv4_for_ipv4_field(self): def test_invalid_ipv6_for_ipv6_field(self): """Test that an invalid IPv6 raises only an IPv6-related error.""" - self.model._meta.get_field("address").protocol = "IPv6" # Set field to IPv6 only invalid_data = {"address": "invalid-ip"} - serializer = self.serializer_class(data=invalid_data) + serializer = self.ipv6_serializer(data=invalid_data) + + with self.assertRaises(ValidationError) as context: + serializer.is_valid(raise_exception=True) + + self.assertEqual( + str(context.exception.detail["address"][0]), + "Enter a valid IPv6 address." + ) + + def test_invalid_ipv6_message_v1(self): + """Test that an invalid IPv6 raises error message when data contains ':' in it.""" + invalid_data = {"address": "invalid : data"} + serializer = self.ipv6_serializer(data=invalid_data) + + with self.assertRaises(ValidationError) as context: + serializer.is_valid(raise_exception=True) + + self.assertEqual( + str(context.exception.detail["address"][0]), + "Enter a valid IPv4 or IPv6 address." + ) + + def test_invalid_ipv6_message_v2(self): + """Test that an invalid IPv6 raises error message when data doesn't contains ':' in it.""" + invalid_data = {"address": "invalid-ip"} + serializer = self.ipv6_serializer(data=invalid_data) with self.assertRaises(ValidationError) as context: serializer.is_valid(raise_exception=True) @@ -460,9 +504,8 @@ def test_invalid_ipv6_for_ipv6_field(self): def test_invalid_both_protocol(self): """Test that an invalid IP raises a combined error message when protocol is both.""" - self.model._meta.get_field("address").protocol = "both" # Allow both IPv4 & IPv6 invalid_data = {"address": "invalid-ip"} - serializer = self.serializer_class(data=invalid_data) + serializer = self.both_protocols_serializer(data=invalid_data) with self.assertRaises(ValidationError) as context: serializer.is_valid(raise_exception=True) @@ -474,30 +517,26 @@ def test_invalid_both_protocol(self): def test_valid_ipv4(self): """Test that a valid IPv4 passes validation.""" - self.model._meta.get_field("address").protocol = "IPv4" valid_data = {"address": "192.168.1.1"} - serializer = self.serializer_class(data=valid_data) + serializer = self.ipv4_serializer(data=valid_data) self.assertTrue(serializer.is_valid()) def test_valid_ipv6(self): """Test that a valid IPv6 passes validation.""" - self.model._meta.get_field("address").protocol = "IPv6" valid_data = {"address": "2001:db8::ff00:42:8329"} - serializer = self.serializer_class(data=valid_data) + serializer = self.ipv6_serializer(data=valid_data) self.assertTrue(serializer.is_valid()) def test_valid_ipv4_for_both_protocol(self): """Test that a valid IPv4 is accepted when protocol is 'both'.""" - self.model._meta.get_field("address").protocol = "both" valid_data = {"address": "192.168.1.1"} - serializer = self.serializer_class(data=valid_data) + serializer = self.both_protocols_serializer(data=valid_data) self.assertTrue(serializer.is_valid()) def test_valid_ipv6_for_both_protocol(self): """Test that a valid IPv6 is accepted when protocol is 'both'.""" - self.model._meta.get_field("address").protocol = "both" valid_data = {"address": "2001:db8::ff00:42:8329"} - serializer = self.serializer_class(data=valid_data) + serializer = self.both_protocols_serializer(data=valid_data) self.assertTrue(serializer.is_valid())