From 12d2bec69a3865ffc61e40cd5e145e4d96ec7b7d Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Sat, 1 Mar 2025 00:22:52 +0530 Subject: [PATCH 1/7] Proper Error raise in Reauthentication Form --- warehouse/accounts/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/warehouse/accounts/views.py b/warehouse/accounts/views.py index 38c186dc9c62..668638d5524a 100644 --- a/warehouse/accounts/views.py +++ b/warehouse/accounts/views.py @@ -1515,6 +1515,7 @@ def profile_public_email(user, request): @view_config( route_name="accounts.reauthenticate", + renderer="re-auth.html", uses_session=True, require_csrf=True, require_methods=False, @@ -1557,8 +1558,11 @@ def reauthenticate(request, _form_class=ReAuthenticateForm): request.session.record_password_timestamp( user_service.get_password_timestamp(request.user.id) ) + return resp - return resp + return { + "form": form, + } @view_defaults( From 58321150069b631ca273e2f3227edc27b0d5cec9 Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Sun, 20 Apr 2025 11:00:17 +0530 Subject: [PATCH 2/7] error query param in reautentication --- tests/unit/accounts/test_views.py | 2 ++ tests/unit/manage/test_init.py | 1 + warehouse/accounts/views.py | 36 ++++++++++++++++++------------- warehouse/manage/__init__.py | 12 ++++++++++- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/tests/unit/accounts/test_views.py b/tests/unit/accounts/test_views.py index 80182a3a48d5..e005759a6787 100644 --- a/tests/unit/accounts/test_views.py +++ b/tests/unit/accounts/test_views.py @@ -3423,12 +3423,14 @@ def test_reauth(self, monkeypatch, pyramid_request, pyramid_services, next_route pyramid_request.matched_route = pretend.stub(name=pretend.stub()) pyramid_request.matchdict = {"foo": "bar"} pyramid_request.GET = pretend.stub(mixed=lambda: {"baz": "bar"}) + pyramid_request.params = {} form_obj = pretend.stub( next_route=pretend.stub(data=next_route), next_route_matchdict=pretend.stub(data="{}"), next_route_query=pretend.stub(data="{}"), validate=lambda: True, + password=pretend.stub(errors=[]), ) form_class = pretend.call_recorder(lambda d, **kw: form_obj) diff --git a/tests/unit/manage/test_init.py b/tests/unit/manage/test_init.py index 18e0395b618e..3b194c0f3ebc 100644 --- a/tests/unit/manage/test_init.py +++ b/tests/unit/manage/test_init.py @@ -66,6 +66,7 @@ def test_reauth(self, monkeypatch, require_reauth, needs_reauth_calls): session=pretend.stub( needs_reauthentication=pretend.call_recorder(lambda *args: True) ), + params={}, user=pretend.stub(username=pretend.stub()), matched_route=pretend.stub(name=pretend.stub()), matchdict={"foo": "bar"}, diff --git a/warehouse/accounts/views.py b/warehouse/accounts/views.py index 668638d5524a..5e6c74eb4902 100644 --- a/warehouse/accounts/views.py +++ b/warehouse/accounts/views.py @@ -1515,7 +1515,6 @@ def profile_public_email(user, request): @view_config( route_name="accounts.reauthenticate", - renderer="re-auth.html", uses_session=True, require_csrf=True, require_methods=False, @@ -1542,27 +1541,34 @@ def reauthenticate(request, _form_class=ReAuthenticateForm): ], ) - if form.next_route.data and form.next_route_matchdict.data: - redirect_to = request.route_path( - form.next_route.data, - **json.loads(form.next_route_matchdict.data) - | dict(_query=json.loads(form.next_route_query.data)), - ) - else: - redirect_to = request.route_path("manage.projects") + next_route = form.next_route.data or "manage.projects" + next_route_matchdict = json.loads(form.next_route_matchdict.data or "{}") + next_route_query = json.loads(form.next_route_query.data or "{}") - resp = HTTPSeeOther(redirect_to) + is_valid = form.validate() - if request.method == "POST" and form.validate(): + # Ensure errors don't persist across successful validations + next_route_query.pop("errors", None) + + if request.method == "POST" and is_valid: request.session.record_auth_timestamp() request.session.record_password_timestamp( user_service.get_password_timestamp(request.user.id) ) - return resp + else: + # Inject password errors into query if validation failed + if form.password.errors: + next_route_query["errors"] = json.dumps({ + "password": [str(e) for e in form.password.errors] + }) + + redirect_to = request.route_path( + next_route, + **next_route_matchdict, + _query=next_route_query, + ) - return { - "form": form, - } + return HTTPSeeOther(redirect_to) @view_defaults( diff --git a/warehouse/manage/__init__.py b/warehouse/manage/__init__.py index a06201ee7891..ea6353c53663 100644 --- a/warehouse/manage/__init__.py +++ b/warehouse/manage/__init__.py @@ -35,7 +35,6 @@ def reauth_view(view, info): def wrapped(context, request): if request.session.needs_reauthentication(time_to_reauth): user_service = request.find_service(IUserService, context=None) - form = ReAuthenticateForm( request.POST, request=request, @@ -45,6 +44,17 @@ def wrapped(context, request): next_route_query=json.dumps(request.GET.mixed()), user_service=user_service, ) + errors_param = request.params.get("errors") + if errors_param: + try: + parsed_errors = json.loads(errors_param) + for field_name, messages in parsed_errors.items(): + field = getattr(form, field_name, None) + if field is not None and hasattr(field, "errors"): + field.errors = list(messages) + except (ValueError, TypeError): + # log or ignore bad JSON + pass return render_to_response( "re-auth.html", From dfe5a2f3e47c5640c02b805f46a65dbfa690aacb Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Sun, 20 Apr 2025 11:42:28 +0530 Subject: [PATCH 3/7] Ran make translations --- warehouse/locale/messages.pot | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index d374e0a1ec0c..9f1015f0d745 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -301,28 +301,28 @@ msgstr "" msgid "Please review our updated Terms of Service." msgstr "" -#: warehouse/accounts/views.py:1663 warehouse/accounts/views.py:1905 +#: warehouse/accounts/views.py:1673 warehouse/accounts/views.py:1915 #: warehouse/manage/views/__init__.py:1419 msgid "" "Trusted publishing is temporarily disabled. See https://pypi.org/help" "#admin-intervention for details." msgstr "" -#: warehouse/accounts/views.py:1684 +#: warehouse/accounts/views.py:1694 msgid "disabled. See https://pypi.org/help#admin-intervention for details." msgstr "" -#: warehouse/accounts/views.py:1700 +#: warehouse/accounts/views.py:1710 msgid "" "You must have a verified email in order to register a pending trusted " "publisher. See https://pypi.org/help#openid-connect for details." msgstr "" -#: warehouse/accounts/views.py:1713 +#: warehouse/accounts/views.py:1723 msgid "You can't register more than 3 pending trusted publishers at once." msgstr "" -#: warehouse/accounts/views.py:1728 warehouse/manage/views/__init__.py:1600 +#: warehouse/accounts/views.py:1738 warehouse/manage/views/__init__.py:1600 #: warehouse/manage/views/__init__.py:1715 #: warehouse/manage/views/__init__.py:1829 #: warehouse/manage/views/__init__.py:1941 @@ -331,29 +331,29 @@ msgid "" "again later." msgstr "" -#: warehouse/accounts/views.py:1738 warehouse/manage/views/__init__.py:1613 +#: warehouse/accounts/views.py:1748 warehouse/manage/views/__init__.py:1613 #: warehouse/manage/views/__init__.py:1728 #: warehouse/manage/views/__init__.py:1842 #: warehouse/manage/views/__init__.py:1954 msgid "The trusted publisher could not be registered" msgstr "" -#: warehouse/accounts/views.py:1753 +#: warehouse/accounts/views.py:1763 msgid "" "This trusted publisher has already been registered. Please contact PyPI's" " admins if this wasn't intentional." msgstr "" -#: warehouse/accounts/views.py:1780 +#: warehouse/accounts/views.py:1790 msgid "Registered a new pending publisher to create " msgstr "" -#: warehouse/accounts/views.py:1918 warehouse/accounts/views.py:1931 -#: warehouse/accounts/views.py:1938 +#: warehouse/accounts/views.py:1928 warehouse/accounts/views.py:1941 +#: warehouse/accounts/views.py:1948 msgid "Invalid publisher ID" msgstr "" -#: warehouse/accounts/views.py:1945 +#: warehouse/accounts/views.py:1955 msgid "Removed trusted publisher for project " msgstr "" From ecfac284fbe82f00809c38896567a18c48686762 Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Sun, 20 Apr 2025 11:51:51 +0530 Subject: [PATCH 4/7] Ran make translations --- warehouse/accounts/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/warehouse/accounts/views.py b/warehouse/accounts/views.py index 5e6c74eb4902..b6acc023c913 100644 --- a/warehouse/accounts/views.py +++ b/warehouse/accounts/views.py @@ -1558,9 +1558,9 @@ def reauthenticate(request, _form_class=ReAuthenticateForm): else: # Inject password errors into query if validation failed if form.password.errors: - next_route_query["errors"] = json.dumps({ - "password": [str(e) for e in form.password.errors] - }) + next_route_query["errors"] = json.dumps( + {"password": [str(e) for e in form.password.errors]} + ) redirect_to = request.route_path( next_route, From 24d25e3fc3057378dce91649ed197dea85a1db54 Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Sun, 20 Apr 2025 15:26:37 +0530 Subject: [PATCH 5/7] WIP --- tests/unit/accounts/test_views.py | 62 +++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/unit/accounts/test_views.py b/tests/unit/accounts/test_views.py index e005759a6787..1ee8530bf696 100644 --- a/tests/unit/accounts/test_views.py +++ b/tests/unit/accounts/test_views.py @@ -3462,6 +3462,68 @@ def test_reauth(self, monkeypatch, pyramid_request, pyramid_services, next_route ) ] + @pytest.mark.parametrize("next_route", [None, "/manage/accounts", "/projects/"]) + def test_reauth_with_password_error_in_query(self, monkeypatch, pyramid_request, pyramid_services, next_route): + user_service = pretend.stub(get_password_timestamp=lambda uid: 0) + response = pretend.stub() + + monkeypatch.setattr(views, "HTTPSeeOther", lambda url: response) + + pyramid_services.register_service(user_service, IUserService, None) + + pyramid_request.route_path = lambda *args, **kwargs: pretend.stub() + pyramid_request.session.record_auth_timestamp = pretend.call_recorder(lambda *args: None) + pyramid_request.session.record_password_timestamp = lambda ts: None + pyramid_request.user = pretend.stub(id=pretend.stub(), username=pretend.stub()) + pyramid_request.matched_route = pretend.stub(name=pretend.stub()) + pyramid_request.matchdict = {"foo": "bar"} + pyramid_request.GET = pretend.stub(mixed=lambda: {"baz": "bar"}) + + # Inject password error through query params + password_errors = ["The password is invalid. Try again."] + # pyramid_request.params = { + # "errors": json.dumps({"password": password_errors}) + # } + + form_obj = pretend.stub( + next_route=pretend.stub(data=next_route), + next_route_matchdict=pretend.stub(data="{}"), + next_route_query=pretend.stub(data="{}"), + validate=lambda: False, # Simulate form validation failure + password=pretend.stub(errors=password_errors), + ) + + form_class = pretend.call_recorder(lambda d, **kw: form_obj) + + if next_route is not None: + pyramid_request.method = "POST" + pyramid_request.POST["next_route"] = next_route + pyramid_request.POST["next_route_matchdict"] = "{}" + pyramid_request.POST["next_route_query"] = "{}" + + _ = views.reauthenticate(pyramid_request, _form_class=form_class) + + # assert pyramid_request.session.record_auth_timestamp.calls == ( + # [pretend.call()] if next_route is not None else [] + # ) + assert form_class.calls == [ + pretend.call( + pyramid_request.POST, + request=pyramid_request, + username=pyramid_request.user.username, + next_route=pyramid_request.matched_route.name, + next_route_matchdict=json.dumps(pyramid_request.matchdict), + next_route_query=json.dumps(pyramid_request.GET.mixed()), + action="reauthenticate", + user_service=user_service, + check_password_metrics_tags=[ + "method:reauth", + "auth_method:reauthenticate_form", + ], + ) + ] + # assert form_obj.password.errors == password_errors + def test_reauth_no_user(self, monkeypatch, pyramid_request): pyramid_request.user = None pyramid_request.route_path = pretend.call_recorder(lambda a: "/the-redirect") From 0245318f4fc16e32ee7bfa43549d9544980d32f3 Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Sun, 20 Apr 2025 18:01:08 +0530 Subject: [PATCH 6/7] WIP --- tests/unit/accounts/test_views.py | 9 +-- tests/unit/manage/test_init.py | 130 ++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 7 deletions(-) diff --git a/tests/unit/accounts/test_views.py b/tests/unit/accounts/test_views.py index 1ee8530bf696..5590c3c7218c 100644 --- a/tests/unit/accounts/test_views.py +++ b/tests/unit/accounts/test_views.py @@ -13,6 +13,7 @@ import datetime import json import uuid +import pyramid.renderers import freezegun import pretend @@ -3481,9 +3482,6 @@ def test_reauth_with_password_error_in_query(self, monkeypatch, pyramid_request, # Inject password error through query params password_errors = ["The password is invalid. Try again."] - # pyramid_request.params = { - # "errors": json.dumps({"password": password_errors}) - # } form_obj = pretend.stub( next_route=pretend.stub(data=next_route), @@ -3503,9 +3501,6 @@ def test_reauth_with_password_error_in_query(self, monkeypatch, pyramid_request, _ = views.reauthenticate(pyramid_request, _form_class=form_class) - # assert pyramid_request.session.record_auth_timestamp.calls == ( - # [pretend.call()] if next_route is not None else [] - # ) assert form_class.calls == [ pretend.call( pyramid_request.POST, @@ -3522,7 +3517,7 @@ def test_reauth_with_password_error_in_query(self, monkeypatch, pyramid_request, ], ) ] - # assert form_obj.password.errors == password_errors + def test_reauth_no_user(self, monkeypatch, pyramid_request): pyramid_request.user = None diff --git a/tests/unit/manage/test_init.py b/tests/unit/manage/test_init.py index 3b194c0f3ebc..ba9f28717791 100644 --- a/tests/unit/manage/test_init.py +++ b/tests/unit/manage/test_init.py @@ -12,6 +12,7 @@ import pretend import pytest +import json from warehouse import manage @@ -95,6 +96,135 @@ def view(context, request): assert view.calls == [] assert request.session.needs_reauthentication.calls == needs_reauth_calls + @pytest.mark.parametrize( + ("require_reauth", "needs_reauth_calls"), + [ + (True, [pretend.call(manage.DEFAULT_TIME_TO_REAUTH)]), + (666, [pretend.call(666)]), + ], + ) + def test_reauth_with_errors_param(self, monkeypatch, require_reauth, needs_reauth_calls): + context = pretend.stub() + + class FakeField: + def __init__(self): + self.errors = [] + + class FakeForm: + def __init__(self, *args, **kwargs): + self.password = FakeField() + + request = pretend.stub( + find_service=pretend.call_recorder(lambda service, context: pretend.stub()), + POST=pretend.stub(), + session=pretend.stub( + needs_reauthentication=pretend.call_recorder(lambda *args: True) + ), + params={"errors": json.dumps({"password": ["Invalid password."]})}, + user=pretend.stub(username="test_user"), + matched_route=pretend.stub(name="some_route"), + matchdict={"foo": "bar"}, + GET=pretend.stub(mixed=lambda: {"baz": "qux"}), + ) + + monkeypatch.setattr(manage, "render_to_response", lambda tpl, ctx, request: ctx) + monkeypatch.setattr(manage, "ReAuthenticateForm", lambda *a, **kw: FakeForm()) + + @pretend.call_recorder + def view(context, request): + raise AssertionError("View should not be called when reauth is needed") + + info = pretend.stub(options={"require_reauth": True}, exception_only=False) + derived_view = manage.reauth_view(view, info) + + result = derived_view(context, request) + # Ensure error message is correctly set in the form field + assert result["form"].password.errors == ["Invalid password."] + + @pytest.mark.parametrize( + ("require_reauth", "needs_reauth_calls"), + [ + (True, [pretend.call(manage.DEFAULT_TIME_TO_REAUTH)]), + (666, [pretend.call(666)]), + ], + ) + def test_reauth_view_with_malformed_errors(self, monkeypatch, require_reauth, needs_reauth_calls): + mock_user_service = pretend.stub() + mock_form = pretend.stub(password=pretend.stub(errors=[])) + + monkeypatch.setattr(manage, "ReAuthenticateForm", lambda *a, **k: mock_form) + monkeypatch.setattr(manage, "render_to_response", lambda tpl, context, request=None: context) + + context = pretend.stub() + dummy_request = pretend.stub( + session=pretend.stub( + needs_reauthentication=pretend.call_recorder(lambda *a: True) + ), + params={"errors": "{this is not: valid json"}, + POST={}, + user=pretend.stub(username="fakeuser"), + matched_route=pretend.stub(name="fake.route"), + matchdict={"foo": "bar"}, + GET=pretend.stub(mixed=lambda: {"baz": "qux"}), + find_service=lambda service, context=None: mock_user_service, + ) + + view = lambda context, request: None + info = pretend.stub(options={"require_reauth": True}, exception_only=False) + + derived_view = manage.reauth_view(view, info) + result = derived_view(context, dummy_request) + + assert "form" in result + assert result["form"] == mock_form + # Since the JSON is invalid, errors shouldn't be set/modified + assert mock_form.password.errors == [] + + @pytest.mark.parametrize( + ("require_reauth", "needs_reauth_calls"), + [ + (True, [pretend.call(manage.DEFAULT_TIME_TO_REAUTH)]), + (666, [pretend.call(666)]), + ], + ) + def test_reauth_view_sets_errors(self, monkeypatch, require_reauth, needs_reauth_calls): + # Step 1: Mock the field that should have errors + mock_field = pretend.stub(errors=[]) + + # Step 2: Mock the form that has the password field + form = pretend.stub(password=mock_field) + + # Step 3: Ensure that ReAuthenticateForm uses this mocked form + monkeypatch.setattr(manage, "ReAuthenticateForm", lambda *a, **kw: form) + + # Step 4: Mock the render_to_response function (doesn't matter for this test) + monkeypatch.setattr(manage, "render_to_response", lambda *a, **kw: {}) + + # Step 5: Define the request and the parameters that simulate the test case + request = pretend.stub( + session=pretend.stub(needs_reauthentication=lambda *a: True), + params={"errors": json.dumps({"password": ["Invalid password"]})}, # mock errors + POST={}, + GET=pretend.stub(mixed=lambda: {}), + matched_route=pretend.stub(name="reauth"), + matchdict={}, + user=pretend.stub(username="tester"), + find_service=lambda *a, **kw: pretend.stub(), + ) + + context = pretend.stub() + info = pretend.stub(options={"require_reauth": True}) + view = lambda context, request: None # Mock view function + + # Step 6: Wrap the view with the reauth_view + wrapped = manage.reauth_view(view, info) + + # Step 7: Call the wrapped view + wrapped(context, request) + + # Step 8: Confirm that the field.errors were set correctly + assert mock_field.errors == ["Invalid password"], f"Expected errors to be ['Invalid password'], but got {mock_field.errors}" + def test_includeme(monkeypatch): settings = { From 8d635deb616be7f1dc31a89077257c063866aba3 Mon Sep 17 00:00:00 2001 From: Daksh Bhayana Date: Sun, 20 Apr 2025 21:59:12 +0530 Subject: [PATCH 7/7] Added test case for coverage --- tests/unit/accounts/test_views.py | 10 +- tests/unit/manage/test_init.py | 154 +++++++++++++++--------------- 2 files changed, 84 insertions(+), 80 deletions(-) diff --git a/tests/unit/accounts/test_views.py b/tests/unit/accounts/test_views.py index 5590c3c7218c..e21befcba4c3 100644 --- a/tests/unit/accounts/test_views.py +++ b/tests/unit/accounts/test_views.py @@ -13,7 +13,6 @@ import datetime import json import uuid -import pyramid.renderers import freezegun import pretend @@ -3464,7 +3463,9 @@ def test_reauth(self, monkeypatch, pyramid_request, pyramid_services, next_route ] @pytest.mark.parametrize("next_route", [None, "/manage/accounts", "/projects/"]) - def test_reauth_with_password_error_in_query(self, monkeypatch, pyramid_request, pyramid_services, next_route): + def test_reauth_with_password_error_in_query( + self, monkeypatch, pyramid_request, pyramid_services, next_route + ): user_service = pretend.stub(get_password_timestamp=lambda uid: 0) response = pretend.stub() @@ -3473,7 +3474,9 @@ def test_reauth_with_password_error_in_query(self, monkeypatch, pyramid_request, pyramid_services.register_service(user_service, IUserService, None) pyramid_request.route_path = lambda *args, **kwargs: pretend.stub() - pyramid_request.session.record_auth_timestamp = pretend.call_recorder(lambda *args: None) + pyramid_request.session.record_auth_timestamp = pretend.call_recorder( + lambda *args: None + ) pyramid_request.session.record_password_timestamp = lambda ts: None pyramid_request.user = pretend.stub(id=pretend.stub(), username=pretend.stub()) pyramid_request.matched_route = pretend.stub(name=pretend.stub()) @@ -3518,7 +3521,6 @@ def test_reauth_with_password_error_in_query(self, monkeypatch, pyramid_request, ) ] - def test_reauth_no_user(self, monkeypatch, pyramid_request): pyramid_request.user = None pyramid_request.route_path = pretend.call_recorder(lambda a: "/the-redirect") diff --git a/tests/unit/manage/test_init.py b/tests/unit/manage/test_init.py index ba9f28717791..e478704173ef 100644 --- a/tests/unit/manage/test_init.py +++ b/tests/unit/manage/test_init.py @@ -10,9 +10,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json + import pretend import pytest -import json from warehouse import manage @@ -103,57 +104,20 @@ def view(context, request): (666, [pretend.call(666)]), ], ) - def test_reauth_with_errors_param(self, monkeypatch, require_reauth, needs_reauth_calls): - context = pretend.stub() - - class FakeField: - def __init__(self): - self.errors = [] - - class FakeForm: - def __init__(self, *args, **kwargs): - self.password = FakeField() - - request = pretend.stub( - find_service=pretend.call_recorder(lambda service, context: pretend.stub()), - POST=pretend.stub(), - session=pretend.stub( - needs_reauthentication=pretend.call_recorder(lambda *args: True) - ), - params={"errors": json.dumps({"password": ["Invalid password."]})}, - user=pretend.stub(username="test_user"), - matched_route=pretend.stub(name="some_route"), - matchdict={"foo": "bar"}, - GET=pretend.stub(mixed=lambda: {"baz": "qux"}), - ) - - monkeypatch.setattr(manage, "render_to_response", lambda tpl, ctx, request: ctx) - monkeypatch.setattr(manage, "ReAuthenticateForm", lambda *a, **kw: FakeForm()) - - @pretend.call_recorder - def view(context, request): - raise AssertionError("View should not be called when reauth is needed") - - info = pretend.stub(options={"require_reauth": True}, exception_only=False) - derived_view = manage.reauth_view(view, info) + def test_reauth_view_with_malformed_errors( + self, monkeypatch, require_reauth, needs_reauth_calls + ): + mock_user_service = pretend.stub() + response = pretend.stub() - result = derived_view(context, request) - # Ensure error message is correctly set in the form field - assert result["form"].password.errors == ["Invalid password."] + def mock_response(*args, **kwargs): + return {"mock_key": "mock_response"} - @pytest.mark.parametrize( - ("require_reauth", "needs_reauth_calls"), - [ - (True, [pretend.call(manage.DEFAULT_TIME_TO_REAUTH)]), - (666, [pretend.call(666)]), - ], - ) - def test_reauth_view_with_malformed_errors(self, monkeypatch, require_reauth, needs_reauth_calls): - mock_user_service = pretend.stub() - mock_form = pretend.stub(password=pretend.stub(errors=[])) + def mock_form(*args, **kwargs): + return pretend.stub(password=pretend.stub(errors=[])) - monkeypatch.setattr(manage, "ReAuthenticateForm", lambda *a, **k: mock_form) - monkeypatch.setattr(manage, "render_to_response", lambda tpl, context, request=None: context) + monkeypatch.setattr(manage, "render_to_response", mock_response) + monkeypatch.setattr(manage, "ReAuthenticateForm", mock_form) context = pretend.stub() dummy_request = pretend.stub( @@ -169,41 +133,30 @@ def test_reauth_view_with_malformed_errors(self, monkeypatch, require_reauth, ne find_service=lambda service, context=None: mock_user_service, ) - view = lambda context, request: None + @pretend.call_recorder + def view(context, request): + return response + info = pretend.stub(options={"require_reauth": True}, exception_only=False) derived_view = manage.reauth_view(view, info) - result = derived_view(context, dummy_request) - assert "form" in result - assert result["form"] == mock_form - # Since the JSON is invalid, errors shouldn't be set/modified - assert mock_form.password.errors == [] + assert derived_view(context, dummy_request) is not response + assert mock_form().password.errors == [] - @pytest.mark.parametrize( - ("require_reauth", "needs_reauth_calls"), - [ - (True, [pretend.call(manage.DEFAULT_TIME_TO_REAUTH)]), - (666, [pretend.call(666)]), - ], - ) - def test_reauth_view_sets_errors(self, monkeypatch, require_reauth, needs_reauth_calls): - # Step 1: Mock the field that should have errors + def test_reauth_view_sets_errors(self, monkeypatch): mock_field = pretend.stub(errors=[]) - - # Step 2: Mock the form that has the password field form = pretend.stub(password=mock_field) + response = pretend.stub() - # Step 3: Ensure that ReAuthenticateForm uses this mocked form monkeypatch.setattr(manage, "ReAuthenticateForm", lambda *a, **kw: form) - - # Step 4: Mock the render_to_response function (doesn't matter for this test) monkeypatch.setattr(manage, "render_to_response", lambda *a, **kw: {}) - # Step 5: Define the request and the parameters that simulate the test case request = pretend.stub( session=pretend.stub(needs_reauthentication=lambda *a: True), - params={"errors": json.dumps({"password": ["Invalid password"]})}, # mock errors + params={ + "errors": json.dumps({"password": ["Invalid password"]}) + }, # mock errors POST={}, GET=pretend.stub(mixed=lambda: {}), matched_route=pretend.stub(name="reauth"), @@ -214,16 +167,65 @@ def test_reauth_view_sets_errors(self, monkeypatch, require_reauth, needs_reauth context = pretend.stub() info = pretend.stub(options={"require_reauth": True}) - view = lambda context, request: None # Mock view function - # Step 6: Wrap the view with the reauth_view + @pretend.call_recorder + def view(context, request): + return response + wrapped = manage.reauth_view(view, info) - # Step 7: Call the wrapped view wrapped(context, request) - # Step 8: Confirm that the field.errors were set correctly - assert mock_field.errors == ["Invalid password"], f"Expected errors to be ['Invalid password'], but got {mock_field.errors}" + assert mock_field.errors == [ + "Invalid password" + ], f"Expected errors to be ['Invalid password'], but got {mock_field.errors}" + + def test_reauth_view_field_missing_or_no_errors(self, monkeypatch): + mock_user_service = pretend.stub() + response = pretend.stub() + + def mock_response(*args, **kwargs): + return {"mock_key": "mock_response"} + + class DummyField: + pass # No `errors` attribute + + class DummyForm: + def __init__(self, *args, **kwargs): + self.existing_field = DummyField() # Has no `.errors` + + monkeypatch.setattr(manage, "render_to_response", mock_response) + monkeypatch.setattr(manage, "ReAuthenticateForm", DummyForm) + + context = pretend.stub() + dummy_request = pretend.stub( + session=pretend.stub( + needs_reauthentication=pretend.call_recorder(lambda *a: True) + ), + params={ + "errors": json.dumps( + {"non_existing_field": ["err1"], "existing_field": ["err2"]} + ) + }, + POST={}, + user=pretend.stub(username="fakeuser"), + matched_route=pretend.stub(name="fake.route"), + matchdict={"foo": "bar"}, + GET=pretend.stub(mixed=lambda: {"baz": "qux"}), + find_service=lambda service, context=None: mock_user_service, + ) + + @pretend.call_recorder + def view(context, request): + return response + + info = pretend.stub(options={"require_reauth": True}, exception_only=False) + + derived_view = manage.reauth_view(view, info) + result = derived_view(context, dummy_request) + + assert isinstance(result, dict) + assert result["mock_key"] == "mock_response" def test_includeme(monkeypatch):