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)
diff --git a/tests/test_mock_views.py b/tests/test_mock_views.py
index 8ca8d9d..d78e680 100644
--- a/tests/test_mock_views.py
+++ b/tests/test_mock_views.py
@@ -105,7 +105,7 @@ def test_post_redirects_and_authenticates(self):
url = f"{url}?{urlencode(params)}"
data = {
- "auth_name": "123456789",
+ "auth_name": "296648875",
"auth_pass": "bar",
}
# post our password to the IDP
@@ -119,7 +119,7 @@ def test_post_redirects_and_authenticates(self):
response = self.client.get(response["Location"], follow=False)
User = get_user_model()
- user = User.digid_objects.get(bsn="123456789")
+ user = User.digid_objects.get(bsn="296648875")
# follow redirect to 'next'
self.assertRedirects(response, reverse("test-success"))
@@ -128,7 +128,7 @@ def test_post_redirects_and_authenticates(self):
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Je bent ingelogged als gebruiker")
self.assertContains(response, "{}
".format(str(user)))
- self.assertContains(response, "123456789
")
+ self.assertContains(response, "296648875
")
def test_post_redirect_retains_acs_querystring_params(self):
url = reverse("digid-mock:password")
@@ -140,7 +140,7 @@ def test_post_redirect_retains_acs_querystring_params(self):
url = f"{url}?{urlencode(params)}"
data = {
- "auth_name": "123456789",
+ "auth_name": "296648875",
"auth_pass": "bar",
}
# post our password to the IDP
@@ -150,7 +150,7 @@ def test_post_redirect_retains_acs_querystring_params(self):
expected_redirect = furl(reverse("digid:acs")).set(
{
"foo": "bar",
- "bsn": "123456789",
+ "bsn": "296648875",
"next": reverse("test-success"),
}
)
@@ -158,35 +158,6 @@ def test_post_redirect_retains_acs_querystring_params(self):
response, str(expected_redirect), fetch_redirect_response=False
)
- def test_backend_rejects_non_numerical_name(self):
- url = reverse("digid-mock:password")
- params = {
- "acs": reverse("digid:acs"),
- "next": reverse("test-success"),
- "cancel": reverse("test-index"),
- }
- url = f"{url}?{urlencode(params)}"
-
- data = {
- "auth_name": "foo",
- "auth_pass": "bar",
- }
- # post our password to the IDP
- response = self.client.post(url, data, follow=False)
-
- # it will redirect to our ACS
- self.assertEqual(response.status_code, 302)
- self.assertIn(reverse("digid:acs"), response["Location"])
-
- # follow the ACS redirect and get/create the user
- response = self.client.get(response["Location"], follow=False)
- self.assertEqual(response.status_code, 302)
- self.assertIn(reverse("test-index"), response["Location"])
-
- User = get_user_model()
- with self.assertRaises(User.DoesNotExist):
- User.digid_objects.get(bsn="foo")
-
@override_settings(**OVERRIDE_SETTINGS)
@modify_settings(**MODIFY_SETTINGS)