From 3c11eef457639029a4a1f057ffd3110b32716866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ebbe=20Ba=C3=9F?= Date: Mon, 19 Oct 2026 10:28:35 +0200 Subject: [PATCH] fix: undo no longer fails with a 400 in a signed-in browser Undo ends every session of the user, including the one that opened the link; saving that deleted session at the end of the request made Django answer 400 although the address had been restored. The view now drops its own session (as sign-out does), also on the unclear-outcome page. Tests for both cases from a signed-in browser. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FocPJ75XPma7TcuYEMy8zm --- portal/profile/views.py | 13 +++++++++++++ portal/tests/test_email_change.py | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/portal/profile/views.py b/portal/profile/views.py index a7d15f3..1206888 100644 --- a/portal/profile/views.py +++ b/portal/profile/views.py @@ -266,5 +266,18 @@ def email_undo(request, token): except DirectoryUnavailable: return _unavailable(request) except services.OutcomeUnknown: + _drop_own_session(request) return render(request, "profile/email_unknown.html", status=500) + _drop_own_session(request) return render(request, "profile/email_undone.html") + + +def _drop_own_session(request): + """Undo ends every session of the user, also the one that opened the link. + + Its row is gone, so saving it at the end of the request would fail with a 400; flushing it + clears the cookie instead. + """ + session = getattr(request, "session", None) + if session is not None: + session.flush() diff --git a/portal/tests/test_email_change.py b/portal/tests/test_email_change.py index 4b2efa3..25d8469 100644 --- a/portal/tests/test_email_change.py +++ b/portal/tests/test_email_change.py @@ -440,6 +440,19 @@ def test_undoing_restores_the_address_ends_the_sessions_and_tells_the_admin(peop assert Client().post(path).status_code == 410 and Client().get(path).status_code == 410 +def test_undoing_from_a_browser_that_is_still_signed_in_works(people, adapters): + path = confirmed_undo_path(adapters) + browser = signed_in(adapters) # the link is opened where the user is signed in + response = browser.post(path) + assert ( + response.status_code == 200 + and "Please change your password now." in response.content.decode() + ) + assert mail_of(adapters) == OLD + assert browser.get("/").status_code == 302 # its own session ended too + assert UserSession.objects.filter(username="alice").count() == 0 + + def test_undoing_also_drops_a_request_made_in_the_meantime(people, adapters): path = confirmed_undo_path(adapters) signed_in(adapters) @@ -593,6 +606,20 @@ def test_a_failure_after_the_undoing_write_still_ends_the_sessions( assert "user=alice" in caplog.text and OLD not in caplog.text +def test_an_unclear_undo_from_a_signed_in_browser_shows_the_unclear_page( + people, adapters, monkeypatch +): + path = confirmed_undo_path(adapters) + browser = signed_in(adapters) + + def broken(*args, **kwargs): + raise RuntimeError("database trouble") + + monkeypatch.setattr(services.mailer, "enqueue", broken) + assert browser.post(path).status_code == 500 # not a 400 from its own ended session + assert browser.get("/").status_code == 302 + + def test_a_failure_before_any_write_leaves_the_request_clean(people, adapters, monkeypatch): client = signed_in(adapters)