diff --git a/app/controllers/password_reset.py b/app/controllers/password_reset.py index 022a7c6..c92521c 100644 --- a/app/controllers/password_reset.py +++ b/app/controllers/password_reset.py @@ -13,6 +13,7 @@ from app.services.passhash import hash_string from app.services.email import send_email, is_configured as email_is_configured from app.services.discord import notify_password_reset_request, notify_password_reset_complete +from app.services.recaptcha import verify as verify_recaptcha from app.controllers.decorators import anonymous_required password_reset_bp = Blueprint('password_reset', __name__) @@ -45,6 +46,11 @@ def forgot_password_post(): flash('Please enter your email address', FLASH_ERROR) return render_template('password_reset/forgot.html') + # Validate reCAPTCHA + if not verify_recaptcha(request): + flash('Verification failed. Please complete the challenge and try again.', FLASH_ERROR) + return render_template('password_reset/forgot.html') + # Check if email service is configured if not email_is_configured(): flash('Password reset is currently unavailable. Please contact support.', FLASH_ERROR) diff --git a/templates/password_reset/forgot.html b/templates/password_reset/forgot.html index 0084cfd..d890e41 100644 --- a/templates/password_reset/forgot.html +++ b/templates/password_reset/forgot.html @@ -18,6 +18,13 @@ + {% if RECAPTCHA_SITEKEY %} + +
+
+
+
+ {% endif %}
Back to Login diff --git a/tests/test_password_reset_captcha.py b/tests/test_password_reset_captcha.py new file mode 100644 index 0000000..86afa8e --- /dev/null +++ b/tests/test_password_reset_captcha.py @@ -0,0 +1,40 @@ +""" +Tests for forgot-password reCAPTCHA handling. +""" + +from flask import Flask +from unittest.mock import patch + +from app.models.errors import ErrNoResult +from app.controllers.password_reset import forgot_password_post + + +class TestPasswordResetCaptcha: + """Focused tests for forgot-password captcha flow.""" + + def setup_method(self): + self.app = Flask(__name__) + self.app.secret_key = 'test-secret' + + def test_forgot_password_rejects_invalid_recaptcha(self): + """Forgot-password should reject invalid reCAPTCHA.""" + with self.app.test_request_context('/forgot-password', method='POST', data={'email': 'test@example.com'}): + with patch('app.controllers.password_reset.quota_exceeded', return_value=False): + with patch('app.controllers.password_reset.verify_recaptcha', return_value=False): + with patch('app.controllers.password_reset.email_is_configured') as email_configured: + with patch('app.controllers.password_reset.render_template', return_value='password_reset/forgot.html'): + response = forgot_password_post() + assert response == 'password_reset/forgot.html' + email_configured.assert_not_called() + + def test_forgot_password_accepts_valid_recaptcha(self): + """Forgot-password should continue flow when reCAPTCHA is valid.""" + with self.app.test_request_context('/forgot-password', method='POST', data={'email': 'test@example.com'}): + with patch('app.controllers.password_reset.quota_exceeded', return_value=False): + with patch('app.controllers.password_reset.verify_recaptcha', return_value=True): + with patch('app.controllers.password_reset.email_is_configured', return_value=True): + with patch('app.controllers.password_reset.email_quota_exceeded', return_value=False): + with patch('app.controllers.password_reset.user_by_mail', side_effect=ErrNoResult('not found')): + with patch('app.controllers.password_reset.render_template', return_value='password_reset/email_sent.html'): + response = forgot_password_post() + assert response == 'password_reset/email_sent.html'