FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects
Commit 355f315e authored by Robin Goodall's avatar Robin Goodall :speech_balloon:
Browse files

fix: catch MultipleObjectsReturned now possible with LIKE query

parent 67f5fd8d
No related branches found
No related tags found
1 merge request!92Change last name checking to simulate (some of) jackdaw behaviour
Pipeline #694389 passed
......@@ -69,9 +69,24 @@ class AccountIdentifierFactory(factory.django.DjangoModelFactory):
model = AccountIdentifier
account_id = factory.SubFactory(AccountFactory)
last_name = factory.LazyAttribute(lambda n: factory.Faker._get_faker().last_name().upper())
date_of_birth = factory.Faker("date_of_birth")
@factory.lazy_attribute
def last_name(self):
# Ideally we'd duplicate what is in AccountDetails but not all Accounts have one
faker = factory.Faker._get_faker()
first_letter = self.account_id.crsid[0].upper()
second_letter = self.account_id.crsid[1].upper()
last_name = faker.last_name()
attempts = 0
max_attempts = 20
while not last_name.startswith(second_letter) and attempts < max_attempts:
last_name = faker.last_name()
attempts += 1
return f"{last_name.upper()} {first_letter}"
@factory.lazy_attribute
def code(self):
code_type = random.choice(["staff", "postgraduate", "pgce", "undergraduate"])
......
......@@ -123,6 +123,14 @@ class TokenRequestSerializer(serializers.Serializer):
account_identifier__date_of_birth=data["date_of_birth"],
account_identifier__code=data["code"],
)
except Account.MultipleObjectsReturned:
logger.warn(
"Multiple accounts found with normalised last name",
last_name=data.get("last_name"),
date_of_birth=data["date_of_birth"],
name_match=name_match,
)
raise InvalidGrantError("Ambiguous token request")
except Account.DoesNotExist:
# then match last_name fields being just name_match
account = Account.objects.get(
......
import random
from string import ascii_uppercase
from unittest import mock
from urllib.parse import urlencode
......@@ -6,6 +8,7 @@ from django.urls import reverse
from knox.settings import knox_settings
from rest_framework import exceptions, parsers, status
from activate_account.factories import AccountFactory, AccountIdentifierFactory
from authentication.constants import SESSION_GRANT_TYPE
from authentication.serializers import TokenRequestSerializer
......@@ -86,7 +89,7 @@ def test_valid_session_grant(client, valid_session_grant_body):
@pytest.mark.parametrize("valid_session_grant_body", ["crsid"], indirect=True)
def test_valid_crsid_case_insensitive(request, client, valid_session_grant_body):
def test_valid_crsid_case_insensitive(client, valid_session_grant_body):
"""CRSId comparison is case insensitive"""
field = "crsid"
assert_is_valid_login(
......@@ -101,7 +104,7 @@ def test_valid_crsid_case_insensitive(request, client, valid_session_grant_body)
@pytest.mark.parametrize("valid_session_grant_body", ["last_name"], indirect=True)
def test_valid_last_name_case_insensitive(request, client, valid_session_grant_body):
def test_valid_last_name_case_insensitive(client, valid_session_grant_body):
"""Last name comparison is case insensitive (due to normalisation)"""
field = "last_name"
assert_is_valid_login(
......@@ -116,7 +119,7 @@ def test_valid_last_name_case_insensitive(request, client, valid_session_grant_b
@pytest.mark.parametrize("valid_session_grant_body", ["last_name"], indirect=True)
def test_valid_last_name_partial(request, client, valid_session_grant_body, faker):
def test_valid_last_name_partial(client, valid_session_grant_body, faker):
"""Last name comparison also matches partial (starting followed by space)"""
field = "last_name"
# Just the first part with random case
......@@ -136,6 +139,35 @@ def test_valid_last_name_partial(request, client, valid_session_grant_body, fake
)
@pytest.mark.parametrize("valid_session_grant_body", ["last_name"], indirect=True)
def test_valid_last_name_partial_multiple(client, valid_session_grant_body, account_identifier):
"""Last name partial comparison could potentially match multiple accounts"""
just_first_part = account_identifier.last_name.split(" ")[0]
# Create another account identifier with the same date of birth, code and first part of last
# name (but different full last name)
while True:
alt_last_name = f"{just_first_part} {random.choice(ascii_uppercase)}"
if alt_last_name != account_identifier.last_name:
break
alt_account = AccountFactory()
AccountIdentifierFactory(
account_id=alt_account,
last_name=alt_last_name,
date_of_birth=account_identifier.date_of_birth,
code=account_identifier.code,
)
assert_is_error(
post_login(
client,
{
**valid_session_grant_body,
"last_name": just_first_part,
},
),
"invalid_grant",
)
@pytest.mark.parametrize("valid_session_grant_body", ["crsid", "last_name"], indirect=True)
def test_missing_field(client, request, valid_session_grant_body):
"""Missing a required field results in 'invalid_request'"""
......
......@@ -33,6 +33,7 @@ def test_missing_crsid_and_last_name():
}
@pytest.mark.django_db
def test_empty_post_normalise_last_name():
data = {
"grant_type": SESSION_GRANT_TYPE,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment