Skip to content

Commit

Permalink
Merge pull request #47 from maykinmedia/feature/mock-validate-bsn
Browse files Browse the repository at this point in the history
Add bsn validation to mock login form
  • Loading branch information
sergei-maertens authored Oct 23, 2023
2 parents ee54cb1 + eda423f commit 6dcf967
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 35 deletions.
7 changes: 6 additions & 1 deletion digid_eherkenning/mock/idp/forms.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
54 changes: 54 additions & 0 deletions digid_eherkenning/validators.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,62 @@
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
oin_validator = RegexValidator(
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."),
}
24 changes: 24 additions & 0 deletions tests/test_mock_forms.py
Original file line number Diff line number Diff line change
@@ -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)
39 changes: 5 additions & 34 deletions tests/test_mock_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"))
Expand All @@ -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, "<code>{}</code>".format(str(user)))
self.assertContains(response, "<code>123456789</code>")
self.assertContains(response, "<code>296648875</code>")

def test_post_redirect_retains_acs_querystring_params(self):
url = reverse("digid-mock:password")
Expand All @@ -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
Expand All @@ -150,43 +150,14 @@ 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"),
}
)
self.assertRedirects(
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)
Expand Down

0 comments on commit 6dcf967

Please sign in to comment.