canaille-globuzma/tests/app/test_forms.py

401 lines
13 KiB
Python
Raw Normal View History

import datetime
from unittest import mock
2023-08-03 16:48:51 +00:00
import pytest
import wtforms
from babel.dates import LOCALTZ
from flask import current_app
from werkzeug.datastructures import ImmutableMultiDict
from canaille.app.forms import DateTimeUTCField
from canaille.app.forms import compromised_password_validator
2024-10-28 21:17:47 +00:00
from canaille.app.forms import password_length_validator
from canaille.app.forms import password_too_long_validator
from canaille.app.forms import phone_number
def test_datetime_utc_field_no_timezone_is_local_timezone(testclient):
current_app.config["CANAILLE"]["TIMEZONE"] = None
class TestForm(wtforms.Form):
dt = DateTimeUTCField()
form = TestForm()
form.validate()
assert form.dt.data is None
utc_date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc)
offset = LOCALTZ.utcoffset(utc_date.replace(tzinfo=None))
locale_date = datetime.datetime(2023, 6, 1, 12) + offset
rendered_locale_date = locale_date.strftime("%Y-%m-%d %H:%M:%S")
rendered_locale_date_form = locale_date.strftime("%Y-%m-%d %H:%M:%S")
request_form = ImmutableMultiDict({"dt": rendered_locale_date_form})
form = TestForm(request_form)
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date_form}">'
)
form = TestForm(data={"dt": utc_date})
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
class Foobar:
dt = utc_date
form = TestForm(obj=Foobar())
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
def test_datetime_utc_field_utc(testclient):
current_app.config["CANAILLE"]["TIMEZONE"] = "UTC"
class TestForm(wtforms.Form):
dt = DateTimeUTCField()
form = TestForm()
form.validate()
assert form.dt.data is None
date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc)
rendered_date = date.strftime("%Y-%m-%d %H:%M:%S")
rendered_date_form = date.strftime("%Y-%m-%d %H:%M:%S")
request_form = ImmutableMultiDict({"dt": rendered_date_form})
form = TestForm(request_form)
assert form.validate()
assert form.dt.data == date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_date_form}">'
)
form = TestForm(data={"dt": date})
assert form.validate()
assert form.dt.data == date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_date}">'
)
class Foobar:
dt = date
form = TestForm(obj=Foobar())
assert form.validate()
assert form.dt.data == date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_date}">'
)
def test_datetime_utc_field_japan_timezone(testclient):
current_app.config["CANAILLE"]["TIMEZONE"] = "Japan"
class TestForm(wtforms.Form):
dt = DateTimeUTCField()
form = TestForm()
form.validate()
assert form.dt.data is None
utc_date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc)
locale_date = datetime.datetime(2023, 6, 1, 21)
rendered_locale_date = locale_date.strftime("%Y-%m-%d %H:%M:%S")
rendered_locale_date_form = locale_date.strftime("%Y-%m-%d %H:%M:%S")
request_form = ImmutableMultiDict({"dt": rendered_locale_date_form})
form = TestForm(request_form)
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date_form}">'
)
form = TestForm(data={"dt": utc_date})
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
class Foobar:
dt = utc_date
form = TestForm(obj=Foobar())
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
def test_datetime_utc_field_invalid_timezone(testclient):
current_app.config["CANAILLE"]["TIMEZONE"] = "invalid"
class TestForm(wtforms.Form):
dt = DateTimeUTCField()
form = TestForm()
form.validate()
assert form.dt.data is None
utc_date = datetime.datetime(2023, 6, 1, 12, tzinfo=datetime.timezone.utc)
offset = LOCALTZ.utcoffset(utc_date.replace(tzinfo=None))
locale_date = datetime.datetime(2023, 6, 1, 12) + offset
rendered_locale_date = locale_date.strftime("%Y-%m-%d %H:%M:%S")
rendered_locale_date_form = locale_date.strftime("%Y-%m-%d %H:%M:%S")
request_form = ImmutableMultiDict({"dt": rendered_locale_date_form})
form = TestForm(request_form)
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date_form}">'
)
form = TestForm(data={"dt": utc_date})
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
class Foobar:
dt = utc_date
form = TestForm(obj=Foobar())
assert form.validate()
assert form.dt.data == utc_date
assert (
form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
)
2023-06-22 09:39:50 +00:00
def test_fieldlist_add_readonly(testclient, logged_user, backend):
testclient.app.config["CANAILLE"]["ACL"]["DEFAULT"]["WRITE"].remove("phone_numbers")
testclient.app.config["CANAILLE"]["ACL"]["DEFAULT"]["READ"].append("phone_numbers")
backend.reload(logged_user)
2023-06-28 11:26:15 +00:00
res = testclient.get("/profile/user")
2023-07-20 16:43:28 +00:00
form = res.forms["baseform"]
assert "readonly" in form["phone_numbers-0"].attrs
assert "phone_numbers-1" not in form.fields
2023-06-28 11:26:15 +00:00
data = {
2023-07-20 16:43:28 +00:00
"csrf_token": form["csrf_token"].value,
"family_name": form["family_name"].value,
"phone_numbers-0": form["phone_numbers-0"].value,
2023-06-28 11:26:15 +00:00
"fieldlist_add": "phone_numbers-0",
}
testclient.post("/profile/user", data, status=403)
def test_fieldlist_remove_readonly(testclient, logged_user, backend):
testclient.app.config["CANAILLE"]["ACL"]["DEFAULT"]["WRITE"].remove("phone_numbers")
testclient.app.config["CANAILLE"]["ACL"]["DEFAULT"]["READ"].append("phone_numbers")
backend.reload(logged_user)
2023-06-28 11:26:15 +00:00
logged_user.phone_numbers = ["555-555-000", "555-555-111"]
backend.save(logged_user)
2023-06-28 11:26:15 +00:00
res = testclient.get("/profile/user")
2023-07-20 16:43:28 +00:00
form = res.forms["baseform"]
assert "readonly" in form["phone_numbers-0"].attrs
assert "readonly" in form["phone_numbers-1"].attrs
2023-06-28 11:26:15 +00:00
data = {
2023-07-20 16:43:28 +00:00
"csrf_token": form["csrf_token"].value,
"family_name": form["family_name"].value,
"phone_numbers-0": form["phone_numbers-0"].value,
2023-06-28 11:26:15 +00:00
"fieldlist_remove": "phone_numbers-1",
}
testclient.post("/profile/user", data, status=403)
2023-06-22 09:39:50 +00:00
def test_inline_validation_invalid_field(testclient, logged_admin, user):
res = testclient.get("/profile")
testclient.post(
"/profile",
{
"csrf_token": res.form["csrf_token"].value,
"email": "john@doe.com",
},
headers={
"HX-Request": "true",
"HX-Trigger-Name": "invalid-field",
},
status=400,
)
2023-08-03 16:48:51 +00:00
def test_phone_number_validator():
class Field:
def __init__(self, data):
self.data = data
phone_number(None, Field("0601060106"))
phone_number(None, Field("06 01 06 01 06"))
phone_number(None, Field(" 06 01 06 01 06 "))
phone_number(None, Field("06-01-06-01-06"))
phone_number(None, Field("06.01.06.01.06"))
phone_number(None, Field("+336 01 06 01 06 "))
phone_number(None, Field("555-000-555"))
with pytest.raises(wtforms.ValidationError):
phone_number(None, Field("invalid"))
2024-10-28 21:17:47 +00:00
def test_minimum_password_length_config(testclient):
class Field:
def __init__(self, data):
self.data = data
current_app.config["CANAILLE"]["MIN_PASSWORD_LENGTH"] = 20
password_length_validator(None, Field("12345678901234567890"))
with pytest.raises(wtforms.ValidationError):
password_length_validator(None, Field("1234567890123456789"))
current_app.config["CANAILLE"]["MIN_PASSWORD_LENGTH"] = 8
password_length_validator(None, Field("12345678"))
with pytest.raises(wtforms.ValidationError):
password_length_validator(None, Field("1234567"))
with pytest.raises(wtforms.ValidationError):
password_length_validator(None, Field("1"))
current_app.config["CANAILLE"]["MIN_PASSWORD_LENGTH"] = 0
password_length_validator(None, Field(""))
current_app.config["CANAILLE"]["MIN_PASSWORD_LENGTH"] = None
password_length_validator(None, Field(""))
def test_password_strength_progress_bar(testclient, logged_user):
res = testclient.get("/profile/user/settings")
res = testclient.post(
"/profile/user/settings",
{
"csrf_token": res.form["csrf_token"].value,
"password1": "i'm a little pea",
2024-10-28 21:17:47 +00:00
},
headers={
"HX-Request": "true",
"HX-Trigger-Name": "password1",
},
)
res.mustcontain('data-percent="100"')
2024-10-28 21:17:47 +00:00
def test_maximum_password_length_config(testclient):
class Field:
def __init__(self, data):
self.data = data
password_too_long_validator(None, Field("a" * 1000))
with pytest.raises(wtforms.ValidationError):
password_too_long_validator(None, Field("a" * 1001))
current_app.config["CANAILLE"]["MAX_PASSWORD_LENGTH"] = 500
password_too_long_validator(None, Field("a" * 500))
with pytest.raises(wtforms.ValidationError):
password_too_long_validator(None, Field("a" * 501))
current_app.config["CANAILLE"]["MAX_PASSWORD_LENGTH"] = None
password_too_long_validator(None, Field("a" * 4096))
with pytest.raises(wtforms.ValidationError):
password_too_long_validator(None, Field("a" * 4097))
current_app.config["CANAILLE"]["MAX_PASSWORD_LENGTH"] = 0
password_too_long_validator(None, Field("a" * 4096))
with pytest.raises(wtforms.ValidationError):
password_too_long_validator(None, Field("a" * 4097))
current_app.config["CANAILLE"]["MAX_PASSWORD_LENGTH"] = 5000
password_too_long_validator(None, Field("a" * 4096))
with pytest.raises(wtforms.ValidationError):
password_too_long_validator(None, Field("a" * 4097))
def test_compromised_password_validator(testclient):
class Field:
def __init__(self, data):
self.data = data
compromised_password_validator(None, Field("i'm a little pea"))
compromised_password_validator(None, Field("i'm a little chickpea"))
compromised_password_validator(None, Field("i'm singing in the rain"))
with pytest.raises(wtforms.ValidationError):
compromised_password_validator(None, Field("password"))
with pytest.raises(wtforms.ValidationError):
compromised_password_validator(None, Field("987654321"))
with pytest.raises(wtforms.ValidationError):
compromised_password_validator(None, Field("correct horse battery staple"))
with pytest.raises(wtforms.ValidationError):
compromised_password_validator(None, Field("zxcvbn123"))
with pytest.raises(wtforms.ValidationError):
compromised_password_validator(None, Field("azertyuiop123"))
@mock.patch("requests.api.get")
def test_compromised_password_validator_with_failure_of_api_request_and_no_SMTP_in_config(
api_get, testclient, logged_user
):
api_get.side_effect = mock.Mock(side_effect=Exception())
current_app.config["CANAILLE"]["SMTP"] = None
class Field:
def __init__(self, data):
self.data = data
compromised_password_validator(None, Field("i'm a little pea"))
compromised_password_validator(None, Field("i'm a little chickpea"))
compromised_password_validator(None, Field("i'm singing in the rain"))
compromised_password_validator(None, Field("password"))
compromised_password_validator(None, Field("987654321"))
compromised_password_validator(None, Field("correct horse battery staple"))
compromised_password_validator(None, Field("zxcvbn123"))
compromised_password_validator(None, Field("azertyuiop123"))
@mock.patch("requests.api.get")
def test_compromised_password_validator_with_failure_of_api_request_and_only_with_htmx(
api_get, testclient, logged_user
):
api_get.side_effect = mock.Mock(side_effect=Exception())
res = testclient.get("/profile/user/settings")
res = testclient.post(
"/profile/user/settings",
{
"csrf_token": res.form["csrf_token"].value,
"password1": "correct horse battery staple",
},
headers={
"HX-Request": "true",
"HX-Trigger-Name": "password1",
},
)
res.mustcontain('data-percent="100"')