Skip to content

Commit c867ac4

Browse files
Document password security policy on the registration page
1 parent 734a89b commit c867ac4

3 files changed

Lines changed: 79 additions & 4 deletions

File tree

routers/core/account.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def validate_password_strength_and_match(
8181
if not COMPILED_PASSWORD_PATTERN.match(password):
8282
raise PasswordValidationError(
8383
field="password",
84-
message="Password does not satisfy the security policy"
84+
message="Password must contain at least 8 characters, including one uppercase letter, one lowercase letter, one number, and one special character"
8585
)
8686

8787
# Validate passwords match

templates/account/register.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@
3232
<!-- Password Input -->
3333
<div class="mb-3">
3434
<label for="password" class="form-label">Password</label>
35-
<input type="password" class="form-control" id="password" name="password"
35+
<input type="password" class="form-control" id="password" name="password"
3636
pattern="{{ password_pattern }}"
37-
title="Must contain at least one number, one uppercase and lowercase letter, one special character, and at least 8 or more characters"
37+
title="Must contain at least one number, one uppercase and lowercase letter, one special character, and at least 8 or more characters"
3838
placeholder="Enter your password" required
3939
autocomplete="new-password">
40+
<div class="form-text">
41+
Password must contain at least 8 characters, including one uppercase letter, one lowercase letter, one number, and one special character.
42+
</div>
4043
<div class="invalid-feedback">
4144
Must contain at least one number, one uppercase and lowercase letter, one special character, and at least 8 or more characters
4245
</div>
@@ -46,7 +49,8 @@
4649
<div class="mb-3">
4750
<label for="confirm_password" class="form-label">Confirm Password</label>
4851
<input type="password" class="form-control" id="confirm_password" name="confirm_password"
49-
placeholder="Confirm your password" required>
52+
placeholder="Confirm your password" required
53+
autocomplete="new-password">
5054
<div class="invalid-feedback">
5155
Passwords do not match.
5256
</div>

tests/routers/core/test_account.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,77 @@ def test_logout_endpoint(auth_client: TestClient):
182182
# --- Error Case Tests ---
183183

184184

185+
def test_register_page_shows_password_requirements(unauth_client: TestClient):
186+
"""Issue #156: Register page must display password requirements visibly."""
187+
response = unauth_client.get(app.url_path_for("read_register"))
188+
assert response.status_code == 200
189+
html = response.text
190+
# Requirements should be visible as text (not just in hidden pattern/title attributes)
191+
assert "8" in html, "Page should mention minimum 8 characters"
192+
assert "uppercase" in html.lower(), "Page should mention uppercase requirement"
193+
assert "lowercase" in html.lower(), "Page should mention lowercase requirement"
194+
assert "number" in html.lower() or "digit" in html.lower(), "Page should mention digit requirement"
195+
assert "special" in html.lower(), "Page should mention special character requirement"
196+
197+
198+
def test_register_page_confirm_password_has_autocomplete(unauth_client: TestClient):
199+
"""Issue #156: Both password fields must have autocomplete='new-password' for Chrome autofill."""
200+
response = unauth_client.get(app.url_path_for("read_register"))
201+
assert response.status_code == 200
202+
html = response.text
203+
# The confirm_password field should have autocomplete="new-password"
204+
assert 'id="confirm_password"' in html
205+
# Find the confirm_password input and check it has autocomplete="new-password"
206+
import re
207+
confirm_input = re.search(r'<input[^>]*id="confirm_password"[^>]*>', html)
208+
assert confirm_input is not None
209+
assert 'autocomplete="new-password"' in confirm_input.group(0), \
210+
"confirm_password field must have autocomplete='new-password' for Chrome autofill"
211+
212+
213+
def test_register_weak_password_error_restates_requirements(unauth_client: TestClient, session: Session):
214+
"""Issue #156: Error toast for weak password must restate the security policy requirements."""
215+
response = unauth_client.post(
216+
app.url_path_for("register"),
217+
data={
218+
"name": "Test User",
219+
"email": "weak@example.com",
220+
"password": "weak",
221+
"confirm_password": "weak"
222+
},
223+
)
224+
assert response.status_code == 422
225+
text = response.text
226+
# The error message must include the actual requirements, not just a generic message
227+
assert "8" in text, "Error should mention minimum 8 characters"
228+
assert "uppercase" in text.lower() or "upper" in text.lower(), \
229+
"Error should mention uppercase requirement"
230+
assert "lowercase" in text.lower() or "lower" in text.lower(), \
231+
"Error should mention lowercase requirement"
232+
233+
234+
def test_register_weak_password_htmx_error_restates_requirements(unauth_client: TestClient, session: Session):
235+
"""Issue #156: HTMX error toast for weak password must restate the security policy requirements."""
236+
response = unauth_client.post(
237+
app.url_path_for("register"),
238+
data={
239+
"name": "Test User",
240+
"email": "weak@example.com",
241+
"password": "weak",
242+
"confirm_password": "weak"
243+
},
244+
headers={"HX-Request": "true"},
245+
)
246+
assert response.status_code == 422
247+
text = response.text
248+
# The toast message must include the actual requirements
249+
assert "8" in text, "Toast should mention minimum 8 characters"
250+
assert "uppercase" in text.lower() or "upper" in text.lower(), \
251+
"Toast should mention uppercase requirement"
252+
assert "lowercase" in text.lower() or "lower" in text.lower(), \
253+
"Toast should mention lowercase requirement"
254+
255+
185256
def test_register_with_existing_email(unauth_client: TestClient, test_account: Account):
186257
response = unauth_client.post(
187258
app.url_path_for("register"),

0 commit comments

Comments
 (0)