diff --git a/digid_eherkenning/mock/idp/forms.py b/digid_eherkenning/mock/idp/forms.py index bd29efb..7de7f18 100644 --- a/digid_eherkenning/mock/idp/forms.py +++ b/digid_eherkenning/mock/idp/forms.py @@ -1,10 +1,15 @@ from django import forms from django.utils.translation import gettext_lazy as _ +from ...validators import BSNValidator + class PasswordLoginForm(forms.Form): auth_name = forms.CharField( - max_length=255, required=True, label=_("DigiD gebruikersnaam") + max_length=255, + required=True, + label=_("DigiD gebruikersnaam"), + validators=[BSNValidator()], ) auth_pass = forms.CharField( max_length=255, required=True, label=_("Wachtwoord"), widget=forms.PasswordInput diff --git a/digid_eherkenning/validators.py b/digid_eherkenning/validators.py index 8fc3eb5..82be08a 100644 --- a/digid_eherkenning/validators.py +++ b/digid_eherkenning/validators.py @@ -1,4 +1,6 @@ +from django.core.exceptions import ValidationError from django.core.validators import RegexValidator +from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ # See `OINType` in eherkenning-dc.xml XSD @@ -6,3 +8,55 @@ regex=r"[0-9]{20}", message=_("A valid OIN consists of 20 digits."), ) + + +validate_digits = RegexValidator( + regex="^[0-9]+$", message=_("Expected a numerical value.") +) + + +class Proef11ValidatorBase: + value_size = NotImplemented + error_messages = { + "too_short": NotImplemented, + "wrong": NotImplemented, + } + + def __call__(self, value): + """ + Validates that a string value is a valid 11-proef number (BSN, RSIN etc) by applying the + '11-proef' checking. + :param value: String object representing a presumably good 11-proef number. + """ + # Initial sanity checks. + validate_digits(value) + if len(value) != self.value_size: + raise ValidationError( + self.error_messages["too_short"], + params={"size": self.value_size}, + code="invalid", + ) + + # 11-proef check. + total = 0 + for multiplier, char in enumerate(reversed(value), start=1): + if multiplier == 1: + total += -multiplier * int(char) + else: + total += multiplier * int(char) + + if total % 11 != 0: + raise ValidationError(self.error_messages["wrong"]) + + +@deconstructible +class BSNValidator(Proef11ValidatorBase): + """ + Validate a BSN value by applying the "11-proef". + """ + + value_size = 9 + error_messages = { + "too_short": _("BSN should have %(size)i characters."), + "wrong": _("Invalid BSN."), + } diff --git a/tests/test_mock_forms.py b/tests/test_mock_forms.py new file mode 100644 index 0000000..454539e --- /dev/null +++ b/tests/test_mock_forms.py @@ -0,0 +1,24 @@ +import pytest + +from digid_eherkenning.mock.idp.forms import PasswordLoginForm + + +@pytest.mark.parametrize( + "auth_name, auth_pass, name_has_error, pass_has_error", + [ + ("296648875", "password", False, False), # OK + ("abcdefghe", "password", True, False), # bsn wrong type + ("2966488759", "password", True, False), # bsn too long + ("29664887", "password", True, False), # bsn too short + ("123456789", "password", True, False), # bsn wrong checksum + ("296648875", "", False, True), # missing password + ], +) +def test_password_login_form_validate( + auth_name, auth_pass, name_has_error, pass_has_error +): + form = PasswordLoginForm(data={"auth_name": auth_name, "auth_pass": auth_pass}) + + assert form.has_error("auth_name") is name_has_error + assert form.has_error("auth_pass") is pass_has_error + assert form.is_valid() is not (name_has_error or pass_has_error)