Skip to content

Commit efb4252

Browse files
committed
Add canEncode validator for use in callback urls and bearer tokens
To validate that we can encode with latin-1. This is needed to stop unicode errors we are seeing in notifications-api when attempting to send to these callback urls. Edited the validator to return a list of characters that cannot be validated. With @karlchillmaid for content work
1 parent f90a324 commit efb4252

3 files changed

Lines changed: 84 additions & 1 deletion

File tree

app/main/forms.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
sentence_case,
7676
)
7777
from app.main.validators import (
78+
CanEncode,
7879
CannotContainURLsOrLinks,
7980
CharactersNotAllowed,
8081
CommonlyUsedPassword,
@@ -2532,11 +2533,16 @@ class CallbackForm(StripWhitespaceForm):
25322533
r"(?:#[\w\-._~%!$&'()*+,;=:@/?]*)?$",
25332534
message="Must be a valid https URL",
25342535
),
2536+
CanEncode(field_type="a web address"),
25352537
],
25362538
)
25372539
bearer_token = GovukPasswordField(
25382540
"Bearer token",
2539-
validators=[DataRequired(message="Cannot be empty"), Length(min=10, thing="the bearer token")],
2541+
validators=[
2542+
DataRequired(message="Cannot be empty"),
2543+
Length(min=10, thing="the bearer token"),
2544+
CanEncode(field_type="a bearer token"),
2545+
],
25402546
)
25412547

25422548
def validate(self, *args, **kwargs):

app/main/validators.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,46 @@
2424
from app.utils.user import is_gov_user
2525

2626

27+
class CanEncode:
28+
"""
29+
Validates that the field data can be encoded into a specific character set.
30+
"""
31+
32+
def __init__(self, encoding="latin-1", field_type=None, message=None):
33+
self.encoding = encoding
34+
self.field_type = field_type
35+
self.message = message
36+
37+
def __call__(self, form, field):
38+
if field.data:
39+
unsupported = set()
40+
for char in field.data:
41+
try:
42+
char.encode(self.encoding)
43+
except UnicodeEncodeError:
44+
unsupported.add(char)
45+
unsupported_char_list = list(unsupported)
46+
if unsupported_char_list:
47+
unsupported_char_list.sort()
48+
49+
field_type = "this field"
50+
if self.field_type is not None:
51+
field_type = self.field_type
52+
53+
if unsupported_char_list != []:
54+
message = self.message
55+
if message is None:
56+
message = (
57+
"You cannot use {} in {}. You must use percent encoding if you want to include {}.".format(
58+
formatted_list(unsupported_char_list, conjunction="or", before_each="", after_each=""),
59+
field_type,
60+
"these characters" if len(unsupported_char_list) > 1 else "this character",
61+
)
62+
)
63+
64+
raise ValidationError(message)
65+
66+
2767
class CommonlyUsedPassword:
2868
def __init__(self, message=None):
2969
if not message:

tests/app/main/test_validators.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from wtforms import ValidationError
66

77
from app.main.validators import (
8+
CanEncode,
89
CharactersNotAllowed,
910
MustContainAlphanumericCharacters,
1011
NoCommasInPlaceHolders,
@@ -217,3 +218,39 @@ def test_string_cannot_contain_string_with_custom_error_message():
217218

218219
assert str(error.value) == "No sequences please"
219220
assert mock_field.error_summary_messages == ["No sequences in %s please"]
221+
222+
223+
@pytest.mark.parametrize(
224+
"data, err_msg",
225+
[
226+
(
227+
"📵 ghi",
228+
"You cannot use 📵 in this field. You must use percent encoding if you want to include this character.",
229+
),
230+
(
231+
"∆ abc 📲",
232+
"You cannot use ∆ or 📲 in this field. You must use percent encoding if you want to include these characters.", # noqa
233+
),
234+
],
235+
)
236+
def test_can_encode_validation(data, err_msg, client_request):
237+
with pytest.raises(ValidationError) as error:
238+
CanEncode()(None, _gen_mock_field(data))
239+
240+
assert str(error.value) == err_msg
241+
242+
243+
def test_string_can_encode_with_custom_field_type():
244+
mock_field = _gen_mock_field("∆ abc 📲", error_summary_messages=[])
245+
with pytest.raises(ValidationError) as error:
246+
CanEncode(field_type="a web address")(None, mock_field)
247+
248+
assert (
249+
str(error.value)
250+
== "You cannot use ∆ or 📲 in a web address. You must use percent encoding if you want to include these characters." # noqa
251+
)
252+
253+
254+
@pytest.mark.parametrize("string", ["", "Résumé", "München"])
255+
def test_string_can_encode_does_not_raise(string):
256+
CanEncode()(None, _gen_mock_field(string))

0 commit comments

Comments
 (0)