Skip to content

Commit

Permalink
[#72] Added backend validation for addressNL component
Browse files Browse the repository at this point in the history
  • Loading branch information
vaszig committed May 30, 2024
1 parent 9b49cd1 commit 76cb94d
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 24 deletions.
28 changes: 4 additions & 24 deletions src/openforms/contrib/brk/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from rest_framework import serializers

from openforms.authentication.constants import AuthAttribute
from openforms.formio.components.custom import AddressValueSerializer
from openforms.submissions.models import Submission
from openforms.validations.base import BasePlugin
from openforms.validations.registry import register
Expand All @@ -32,27 +33,6 @@ def suppress_api_errors(error_message: str) -> Iterator[None]:
raise ValidationError(error_message) from e


class AddressValueSerializer(serializers.Serializer):
postcode = serializers.RegexField(
"^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$",
)
house_number = serializers.RegexField(
r"^\d{1,5}$",
)
house_letter = serializers.RegexField(
"^[a-zA-Z]$", required=False, allow_blank=True
)
house_number_addition = serializers.RegexField(
"^([a-z,A-Z,0-9]){1,4}$",
required=False,
allow_blank=True,
)

def validate_postcode(self, value: str) -> str:
"""Normalize the postcode so that it matches the regex from the BRK API."""
return value.upper().replace(" ", "")


class ValueSerializer(serializers.Serializer):
value = AddressValueSerializer()

Expand Down Expand Up @@ -97,12 +77,12 @@ def __call__(self, value: AddressValue, submission: Submission) -> bool:

address_query: SearchParams = {
"postcode": value["postcode"],
"huisnummer": value["house_number"],
"huisnummer": value["houseNumber"],
}
if "house_letter" in value:
address_query["huisletter"] = value["house_letter"]
address_query["huisletter"] = value["houseLetter"]
if "house_number_addition" in value:
address_query["huisnummertoevoeging"] = value["house_number_addition"]
address_query["huisnummertoevoeging"] = value["houseNumberAddition"]

with client, suppress_api_errors(self.error_messages["retrieving_error"]):
real_estate_objects_resp = client.get_real_estate_by_address(address_query)
Expand Down
35 changes: 35 additions & 0 deletions src/openforms/formio/components/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,11 +377,46 @@ def build_serializer_field(
return serializers.ListField(child=base) if multiple else base


class AddressValueSerializer(serializers.Serializer):
postcode = serializers.RegexField(
"^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$",
)
houseNumber = serializers.RegexField(
r"^\d{1,5}$",
)
houseLetter = serializers.RegexField("^[a-zA-Z]$", required=False, allow_blank=True)
houseNumberAddition = serializers.RegexField(
"^([a-z,A-Z,0-9]){1,4}$",
required=False,
allow_blank=True,
)

def validate_postcode(self, value: str) -> str:
"""Normalize the postcode so that it matches the regex from the BRK API."""
return value.upper().replace(" ", "")


@register("addressNL")
class AddressNL(BasePlugin):

formatter = AddressNLFormatter

def build_serializer_field(self, component: Component) -> AddressValueSerializer:

validate = component.get("validate", {})
required = validate.get("required", False)

extra = {}
validators = []
if plugin_ids := validate.get("plugins", []):
validators.append(PluginValidator(plugin_ids))

extra["validators"] = validators

return AddressValueSerializer(
required=required, allow_null=not required, **extra
)


@register("cosign")
class Cosign(BasePlugin):
Expand Down
163 changes: 163 additions & 0 deletions src/openforms/formio/tests/validation/test_addressnl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from django.test import SimpleTestCase

from rest_framework import serializers

from openforms.contrib.brk.constants import AddressValue
from openforms.contrib.brk.validators import ValueSerializer
from openforms.submissions.models import Submission
from openforms.validations.base import BasePlugin

from ...typing import Component
from .helpers import extract_error, replace_validators_registry, validate_formio_data


class PostcodeValidator(BasePlugin[AddressValue]):
value_serializer = ValueSerializer

def __call__(self, value: AddressValue, submission: Submission):
if value["postcode"] == "1234AA":
raise serializers.ValidationError("nope")


class AddressNLValidationTests(SimpleTestCase):

def test_addressNL_field_required_validation(self):
component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "Required AddressNL",
"validate": {"required": True},
}

invalid_values = [
({}, "required"),
({"addressNl": None}, "null"),
]

for data, error_code in invalid_values:
with self.subTest(data=data):
is_valid, errors = validate_formio_data(component, data)

self.assertFalse(is_valid)
self.assertIn(component["key"], errors)
error = extract_error(errors, component["key"])
self.assertEqual(error.code, error_code)

def test_addressNL_field_non_required_validation(self):
component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "Non required AddressNL",
}

is_valid, _ = validate_formio_data(component, {})

self.assertTrue(is_valid)

def test_addressNL_field_regex_pattern_failure(self):
component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "AddressNL invalid regex",
}

invalid_values = {
"addressNl": {
"postcode": "123456wrong",
"houseNumber": "",
"houseLetter": "A",
"houseNumberAddition": "",
}
}

is_valid, errors = validate_formio_data(component, invalid_values)

self.assertFalse(is_valid)
self.assertIn(component["key"], errors)

error = extract_error(errors["addressNl"], "postcode")

self.assertEqual(error.code, "invalid")

def test_addressNL_field_regex_pattern_success(self):
component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "AddressNL valid pattern",
}

data = {
"addressNl": {
"postcode": "1234AA",
"houseNumber": "2",
"houseLetter": "A",
"houseNumberAddition": "",
}
}

is_valid, _ = validate_formio_data(component, data)

self.assertTrue(is_valid)

def test_missing_keys(self):
component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "AddressNL missing keys",
}

invalid_values = {
"addressNl": {
"houseLetter": "A",
}
}

is_valid, errors = validate_formio_data(component, invalid_values)

postcode_error = extract_error(errors["addressNl"], "postcode")
house_number_error = extract_error(errors["addressNl"], "houseNumber")

self.assertFalse(is_valid)
self.assertEqual(postcode_error.code, "required")
self.assertEqual(house_number_error.code, "required")

def test_plugin_validator(self):
with replace_validators_registry() as register:
register("postcode_validator")(PostcodeValidator)

component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "AddressNL plugin validator",
"validate": {"plugins": ["postcode_validator"]},
}

with self.subTest("valid value"):
is_valid, _ = validate_formio_data(
component,
{
"addressNl": {
"postcode": "9877AA",
"houseNumber": "3",
"houseLetter": "A",
"houseNumberAddition": "",
}
},
)

self.assertTrue(is_valid)

with self.subTest("invalid value"):
is_valid, _ = validate_formio_data(
component,
{
"addressNl": {
"postcode": "1234AA",
"houseNumber": "3",
"houseLetter": "A",
"houseNumberAddition": "",
}
},
)

self.assertFalse(is_valid)
4 changes: 4 additions & 0 deletions src/openforms/submissions/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ class IgnoreDataAndConfigFieldCamelCaseJSONParser(CamelCaseJSONParser):
json_underscoreize = {"ignore_fields": ("data", "configuration")}


class IgnoreValueFieldCamelCaseJSONParser(CamelCaseJSONParser):
json_underscoreize = {"ignore_fields": ("value",)}


class IgnoreDataAndConfigJSONRenderer(CamelCaseJSONRenderer):
# This is needed for fields in the submission step data that have keys with underscores
json_underscoreize = {"ignore_fields": ("data", "configuration")}
114 changes: 114 additions & 0 deletions src/openforms/tests/e2e/test_input_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,3 +934,117 @@ def test_forbidden_file_type(self):

# Make sure the frontend did not create one:
self.assertEqual(TemporaryFileUpload.objects.count(), 1)


class SingleAddressNLTests(ValidationsTestCase):
fuzzy_match_invalid_param_names = True

def assertAddressNLValidationIsAligned(
self,
component: Component,
ui_inputs: dict[str, str],
expected_ui_error: str,
api_value: dict[str, Any],
) -> None:
form = create_form(component)

with self.subTest("frontend validation"):
self._assertAddressNLFrontendValidation(form, ui_inputs, expected_ui_error)

with self.subTest("backend validation"):
self._assertBackendValidation(form, component["key"], api_value)

@async_to_sync
async def _assertAddressNLFrontendValidation(
self, form: Form, ui_inputs: dict[str, str], expected_ui_error: str
) -> None:
frontend_path = reverse("forms:form-detail", kwargs={"slug": form.slug})
url = str(furl(self.live_server_url) / frontend_path)

async with browser_page() as page:
await page.goto(url)
await page.get_by_role("button", name="Formulier starten").click()

for field, value in ui_inputs.items():
await page.fill(f"input[name='{field}']", value)

# try to submit the step which should be invalid, so we expect this to
# render the error message.
await page.get_by_role("button", name="Volgende").click()

await expect(page.get_by_text(expected_ui_error)).to_be_visible()

def test_required_field(self):
component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "Required AddressNL",
"validate": {"required": True},
}

self.assertAddressNLValidationIsAligned(
component,
ui_inputs={},
api_value={
"postcode": "",
"houseNumber": "",
"houseLetter": "",
"houseNumberAddition": "",
},
expected_ui_error="Het verplichte veld Required AddressNL is niet ingevuld.",
)

def test_regex_failure(self):
component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "AddressNL invalid regex",
}

test_cases = [
(
"postcode",
{
"postcode": "1223456Wrong",
"houseNumber": "23",
"houseLetter": "A",
"houseNumberAddition": "",
},
),
(
"houseNumber",
{
"postcode": "1234AA",
"houseNumber": "A",
"houseLetter": "A",
"houseNumberAddition": "",
},
),
(
"houseLetter",
{
"postcode": "1234AA",
"houseNumber": "33",
"houseLetter": "89",
"houseNumberAddition": "",
},
),
(
"houseNumberAddition",
{
"postcode": "1234AA",
"houseNumber": "33",
"houseLetter": "A",
"houseNumberAddition": "9999A",
},
),
]

for field_name, invalid_data in test_cases:
with self.subTest(field_name):
self.assertAddressNLValidationIsAligned(
component,
ui_inputs=invalid_data,
api_value=invalid_data,
expected_ui_error="Ongeldig.",
)
Loading

0 comments on commit 76cb94d

Please sign in to comment.