mirror of
https://github.com/wizarrrr/wizarr.git
synced 2025-12-23 23:59:23 -05:00
feat: enhance username validation and trimming across forms
This commit is contained in:
@@ -3,9 +3,26 @@ from flask_wtf import FlaskForm
|
||||
from wtforms import PasswordField, StringField
|
||||
from wtforms.validators import DataRequired, EqualTo, Length, Optional, Regexp
|
||||
|
||||
from app.forms.validators import (
|
||||
USERNAME_ALLOWED_CHARS_MESSAGE,
|
||||
USERNAME_LENGTH_MESSAGE,
|
||||
USERNAME_MAX_LENGTH,
|
||||
USERNAME_MIN_LENGTH,
|
||||
USERNAME_PATTERN,
|
||||
strip_filter,
|
||||
)
|
||||
|
||||
_username_validators = [
|
||||
DataRequired(),
|
||||
Length(min=3, max=15, message=str(_l("Username must be 3 to 15 characters."))),
|
||||
Length(
|
||||
min=USERNAME_MIN_LENGTH,
|
||||
max=USERNAME_MAX_LENGTH,
|
||||
message=str(_l(USERNAME_LENGTH_MESSAGE)),
|
||||
),
|
||||
Regexp(
|
||||
USERNAME_PATTERN,
|
||||
message=str(_l(USERNAME_ALLOWED_CHARS_MESSAGE)),
|
||||
),
|
||||
]
|
||||
_password_validators = [
|
||||
DataRequired(),
|
||||
@@ -22,7 +39,9 @@ _password_validators = [
|
||||
|
||||
|
||||
class AdminCreateForm(FlaskForm):
|
||||
username = StringField(str(_l("Username")), validators=_username_validators)
|
||||
username = StringField(
|
||||
str(_l("Username")), filters=[strip_filter], validators=_username_validators
|
||||
)
|
||||
password = PasswordField(str(_l("Password")), validators=_password_validators)
|
||||
confirm = PasswordField(
|
||||
str(_l("Confirm password")),
|
||||
@@ -34,7 +53,9 @@ class AdminCreateForm(FlaskForm):
|
||||
|
||||
|
||||
class AdminUpdateForm(FlaskForm):
|
||||
username = StringField(str(_l("Username")), validators=_username_validators)
|
||||
username = StringField(
|
||||
str(_l("Username")), filters=[strip_filter], validators=_username_validators
|
||||
)
|
||||
|
||||
# New password can be left blank (unchanged)
|
||||
password = PasswordField(
|
||||
|
||||
@@ -2,14 +2,33 @@ from flask_wtf import FlaskForm
|
||||
from wtforms import PasswordField, StringField
|
||||
from wtforms.validators import DataRequired, Email, EqualTo, Length, Regexp
|
||||
|
||||
from app.forms.validators import (
|
||||
USERNAME_ALLOWED_CHARS_MESSAGE,
|
||||
USERNAME_LENGTH_MESSAGE,
|
||||
USERNAME_MAX_LENGTH,
|
||||
USERNAME_MIN_LENGTH,
|
||||
USERNAME_PATTERN,
|
||||
strip_filter,
|
||||
)
|
||||
|
||||
|
||||
class JoinForm(FlaskForm):
|
||||
username = StringField(
|
||||
"Username",
|
||||
validators=[DataRequired()],
|
||||
filters=[strip_filter],
|
||||
validators=[
|
||||
DataRequired(),
|
||||
Length(
|
||||
min=USERNAME_MIN_LENGTH,
|
||||
max=USERNAME_MAX_LENGTH,
|
||||
message=USERNAME_LENGTH_MESSAGE,
|
||||
),
|
||||
Regexp(USERNAME_PATTERN, message=USERNAME_ALLOWED_CHARS_MESSAGE),
|
||||
],
|
||||
)
|
||||
email = StringField(
|
||||
"Email",
|
||||
filters=[strip_filter],
|
||||
validators=[DataRequired(), Email()],
|
||||
)
|
||||
password = PasswordField(
|
||||
@@ -32,6 +51,7 @@ class JoinForm(FlaskForm):
|
||||
)
|
||||
code = StringField(
|
||||
"Invite Code",
|
||||
filters=[strip_filter],
|
||||
validators=[DataRequired(), Length(min=6, max=10)],
|
||||
render_kw={"minlength": 6, "maxlength": 10},
|
||||
)
|
||||
|
||||
@@ -2,14 +2,29 @@ from flask_wtf import FlaskForm
|
||||
from wtforms import PasswordField, StringField
|
||||
from wtforms.validators import DataRequired, EqualTo, Length, Regexp
|
||||
|
||||
from app.forms.validators import (
|
||||
USERNAME_ALLOWED_CHARS_MESSAGE,
|
||||
USERNAME_LENGTH_MESSAGE,
|
||||
USERNAME_MAX_LENGTH,
|
||||
USERNAME_MIN_LENGTH,
|
||||
USERNAME_PATTERN,
|
||||
strip_filter,
|
||||
)
|
||||
|
||||
|
||||
class AdminAccountForm(FlaskForm):
|
||||
username = StringField(
|
||||
"Username",
|
||||
validators=[
|
||||
DataRequired(),
|
||||
Length(min=3, max=15, message="Username must be 3 to 15 characters."),
|
||||
Length(
|
||||
min=USERNAME_MIN_LENGTH,
|
||||
max=USERNAME_MAX_LENGTH,
|
||||
message=USERNAME_LENGTH_MESSAGE,
|
||||
),
|
||||
Regexp(USERNAME_PATTERN, message=USERNAME_ALLOWED_CHARS_MESSAGE),
|
||||
],
|
||||
filters=[strip_filter],
|
||||
)
|
||||
password = PasswordField(
|
||||
"Password",
|
||||
|
||||
21
app/forms/validators.py
Normal file
21
app/forms/validators.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Shared form validation constants and filters.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
USERNAME_PATTERN = r"^[\w'.-]+$"
|
||||
USERNAME_MIN_LENGTH = 3
|
||||
USERNAME_MAX_LENGTH = 15
|
||||
USERNAME_LENGTH_MESSAGE = "Username must be 3 to 15 characters."
|
||||
USERNAME_ALLOWED_CHARS_MESSAGE = (
|
||||
"Username can contain letters, numbers, dashes (-), underscores (_), "
|
||||
"apostrophes ('), and periods (.)."
|
||||
)
|
||||
|
||||
|
||||
def strip_filter(value: Any):
|
||||
"""Trim leading/trailing whitespace from string inputs before validation."""
|
||||
if isinstance(value, str):
|
||||
return value.strip()
|
||||
return value
|
||||
75
tests/test_join_form_validation.py
Normal file
75
tests/test_join_form_validation.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from app.forms.join import JoinForm
|
||||
from app.forms.setup import AdminAccountForm
|
||||
from app.forms.validators import USERNAME_ALLOWED_CHARS_MESSAGE
|
||||
|
||||
|
||||
def _join_form_payload(**overrides):
|
||||
base = {
|
||||
"username": "validuser",
|
||||
"email": "user@example.com",
|
||||
"password": "ValidPass1",
|
||||
"confirm_password": "ValidPass1",
|
||||
"code": "ABCDEF",
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
def _admin_form_payload(**overrides):
|
||||
base = {
|
||||
"username": "adminuser",
|
||||
"password": "ValidPass1",
|
||||
"confirm": "ValidPass1",
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
def test_join_form_rejects_spaces_in_username(app):
|
||||
with app.test_request_context(
|
||||
method="POST", data=_join_form_payload(username="invalid user")
|
||||
):
|
||||
form = JoinForm()
|
||||
|
||||
assert not form.validate()
|
||||
assert USERNAME_ALLOWED_CHARS_MESSAGE in form.username.errors
|
||||
|
||||
|
||||
def test_join_form_strips_trailing_whitespace(app):
|
||||
with app.test_request_context(
|
||||
method="POST", data=_join_form_payload(username="validuser ")
|
||||
):
|
||||
form = JoinForm()
|
||||
|
||||
assert form.validate()
|
||||
assert form.username.data == "validuser"
|
||||
|
||||
|
||||
def test_join_form_rejects_invalid_symbols(app):
|
||||
with app.test_request_context(
|
||||
method="POST", data=_join_form_payload(username="bad$user")
|
||||
):
|
||||
form = JoinForm()
|
||||
|
||||
assert not form.validate()
|
||||
assert USERNAME_ALLOWED_CHARS_MESSAGE in form.username.errors
|
||||
|
||||
|
||||
def test_admin_account_form_strips_username_whitespace(app):
|
||||
with app.test_request_context(
|
||||
method="POST", data=_admin_form_payload(username=" adminuser ")
|
||||
):
|
||||
form = AdminAccountForm()
|
||||
|
||||
assert form.validate()
|
||||
assert form.username.data == "adminuser"
|
||||
|
||||
|
||||
def test_admin_account_form_rejects_invalid_username(app):
|
||||
with app.test_request_context(
|
||||
method="POST", data=_admin_form_payload(username="admin user")
|
||||
):
|
||||
form = AdminAccountForm()
|
||||
|
||||
assert not form.validate()
|
||||
assert USERNAME_ALLOWED_CHARS_MESSAGE in form.username.errors
|
||||
Reference in New Issue
Block a user