Skip to content

Fix flaky reset password test with custom Turbo Stream visit action#1474

Closed
maebeale wants to merge 4 commits into
mainfrom
maebeale/fix-reset-password-test
Closed

Fix flaky reset password test with custom Turbo Stream visit action#1474
maebeale wants to merge 4 commits into
mainfrom
maebeale/fix-reset-password-test

Conversation

@maebeale
Copy link
Copy Markdown
Collaborator

@maebeale maebeale commented Apr 4, 2026

Closes #1473

What is the goal of this PR and why is this important?

  • Fix intermittent CI failure in reset_password_person_spec.rb where the user ends up at /users/sign_in instead of / after changing their password
  • Fix confirm dialog failures in both reset_password_person_spec.rb and change_password_flow_spec.rb

How did you approach the change?

Password change form (flaky redirect to sign_in):

  • Turbo 8's fetch()-based form submission with redirect: "follow" has a race condition where the browser may not process the Set-Cookie header from a 303 redirect before making the follow-up GET request — causing Devise to invalidate the session due to a stale authenticatable_salt (which changes when the password changes via update_with_password)
  • Added a custom Turbo Stream visit action that separates session update from navigation: the server responds with a 200 (not a redirect) containing a <turbo-stream action="visit"> tag, the browser processes the Set-Cookie header, then the custom action triggers Turbo.visit() client-side
  • turbo_stream_visit(url) helper in ApplicationController generates the stream tag; Turbo.StreamActions.visit in turbo-events.js handles it client-side
  • This pattern can be reused anywhere a form submission modifies the session and needs to redirect

"Log out and reset it." link (confirm dialog test failures):

  • Fixed change_password_flow_spec.rb to use accept_confirm (wrapping the click) instead of accept_alert (after the click)
  • Added JS-ready waits (have_css("[data-controller='password-toggle']")) to both specs to ensure Turbo is initialized before clicking the confirm link

UI Testing Checklist

  • Change password with correct current password → redirects to home, stays logged in
  • Change password with wrong current password → re-renders form with errors
  • "Log out and reset it." link shows confirm dialog and redirects to password reset page

Anything else to add?

  • Reordered test assertions to check flash content before path — flash presence is a stronger synchronization point than URL check
  • The custom Turbo Stream visit action is a general solution for the Turbo/session race condition — prefer it over data-turbo="false" which disables Turbo for all descendant elements

🤖 Generated with Claude Code

maebeale and others added 2 commits April 4, 2026 19:48
The password change form submitted via Turbo Drive's fetch API, creating
a race condition where the browser might not process the Set-Cookie
header from the bypass_sign_in redirect response before making the
follow-up GET request — causing a stale session with a mismatched
authenticatable_salt and a redirect to /users/sign_in.

Disable Turbo on the form so it uses standard HTML submission, which
reliably processes Set-Cookie between redirect hops. Also reorder test
assertions to check flash content before path.

Closes #1473

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "Log out and reset it." link is inside the password change form.
Adding data-turbo="false" to the form also disabled Turbo for the link,
preventing data-turbo-method and data-turbo-confirm from working.

Fix by adding data-turbo="true" on the link to re-enable Turbo for it.
Also fix change_password_flow_spec to use accept_confirm (wrapping the
click) instead of accept_alert (after the click), and add JS-ready
waits to both specs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@maebeale maebeale changed the title Fix flaky reset password system test Fix flaky reset password system tests Apr 5, 2026
Replace data-turbo="false" approach with a custom Turbo Stream action
that separates session update from navigation. Turbo 8's fetch-based
form submission has a race condition where the browser may not process
the Set-Cookie header from a 303 redirect before making the follow-up
GET request — causing Devise to invalidate the session due to a stale
authenticatable_salt.

The new turbo_stream_visit helper renders a 200 with a Turbo Stream
tag that triggers client-side Turbo.visit(), ensuring the cookie is
set before navigation begins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@maebeale maebeale changed the title Fix flaky reset password system tests Fix flaky reset password test with custom Turbo Stream visit action Apr 5, 2026
// Usage in controller:
// render turbo_stream: turbo_stream_visit(root_path)
Turbo.StreamActions.visit = function () {
Turbo.visit(this.getAttribute("url"))
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This custom Turbo Stream action is the client-side counterpart to turbo_stream_visit in ApplicationController. When the server responds with <turbo-stream action="visit" url="/">, this handler triggers Turbo.visit() — ensuring the browser has already processed the Set-Cookie header from the 200 response before navigation begins.

# (e.g. bypass_sign_in after password change) to ensure the browser
# processes the Set-Cookie header before navigation begins.
def turbo_stream_visit(url)
%(<turbo-stream action="visit" url="#{ERB::Util.html_escape(url)}"></turbo-stream>).html_safe
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper generates a raw Turbo Stream tag rather than using the turbo_stream builder because we need a custom action (visit) not in the standard set. The ERB::Util.html_escape prevents XSS if the URL somehow contains user input.

format.turbo_stream do
flash[:notice] = "Your Password was updated."
render turbo_stream: turbo_stream_visit(root_path)
end
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key change: instead of redirect_to (which Turbo follows via fetch, racing against Set-Cookie processing), we respond with a 200 containing a Turbo Stream that triggers client-side navigation. The browser reliably processes Set-Cookie on the 200 response before the stream action fires.

Document the turbo_stream_visit pattern as the preferred approach for
session-modifying form submissions, and explicitly warn against using
data-turbo="false" which breaks descendant Turbo features.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@maebeale maebeale marked this pull request as ready for review April 5, 2026 02:49
@maebeale maebeale requested a review from jmilljr24 April 5, 2026 12:01
Copy link
Copy Markdown
Collaborator

@jmilljr24 jmilljr24 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to look into it more but these changes make me pause for a second.

Do we actually have an issue with updating passwords or it just the test being flaky? I don't love the idea of adding special code just to make a test past when the application works.

I can see how this change is intended to work but it seems like we are reinventing the wheel. My gut says that a devise password update with turbo and keeping the user logged in is a pretty normal flow.

Here is the default update action from the devise controller. We of course have our own action with some custom stuff like ahoy but for the most part there is nothing special in devise.

  def update
    self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
    prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)

    resource_updated = update_resource(resource, account_update_params)
    yield resource if block_given?
    if resource_updated
      set_flash_message_for_update(resource, prev_unconfirmed_email)
      bypass_sign_in resource, scope: resource_name if sign_in_after_change_password?

      respond_with resource, location: after_update_path_for(resource)
    else
      clean_up_passwords resource
      set_minimum_password_length
      respond_with resource
    end
  end

Maybe respond_with from the responders gem. But again, I'm wonder if this is just a test issue not an app issue.

@maebeale
Copy link
Copy Markdown
Collaborator Author

maebeale commented Apr 5, 2026

I need to look into it more but these changes make me pause for a second.

Do we actually have an issue with updating passwords or it just the test being flaky? I don't love the idea of adding special code just to make a test past when the application works.

I can see how this change is intended to work but it seems like we are reinventing the wheel. My gut says that a devise password update with turbo and keeping the user logged in is a pretty normal flow.

Here is the default update action from the devise controller. We of course have our own action with some custom stuff like ahoy but for the most part there is nothing special in devise.

  def update
    self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
    prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)

    resource_updated = update_resource(resource, account_update_params)
    yield resource if block_given?
    if resource_updated
      set_flash_message_for_update(resource, prev_unconfirmed_email)
      bypass_sign_in resource, scope: resource_name if sign_in_after_change_password?

      respond_with resource, location: after_update_path_for(resource)
    else
      clean_up_passwords resource
      set_minimum_password_length
      respond_with resource
    end
  end

Maybe respond_with from the responders gem. But again, I'm wonder if this is just a test issue not an app issue.

i tagged you in to get your thoughts bc i wasn't sold either. yeah, the test is often flaky and so just trying to get us with a more reliable suite.

@jmilljr24
Copy link
Copy Markdown
Collaborator

I need to look into it more but these changes make me pause for a second.
Do we actually have an issue with updating passwords or it just the test being flaky? I don't love the idea of adding special code just to make a test past when the application works.
I can see how this change is intended to work but it seems like we are reinventing the wheel. My gut says that a devise password update with turbo and keeping the user logged in is a pretty normal flow.
Here is the default update action from the devise controller. We of course have our own action with some custom stuff like ahoy but for the most part there is nothing special in devise.

  def update
    self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
    prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)

    resource_updated = update_resource(resource, account_update_params)
    yield resource if block_given?
    if resource_updated
      set_flash_message_for_update(resource, prev_unconfirmed_email)
      bypass_sign_in resource, scope: resource_name if sign_in_after_change_password?

      respond_with resource, location: after_update_path_for(resource)
    else
      clean_up_passwords resource
      set_minimum_password_length
      respond_with resource
    end
  end

Maybe respond_with from the responders gem. But again, I'm wonder if this is just a test issue not an app issue.

i tagged you in to get your thoughts bc i wasn't sold either. yeah, the test is often flaky and so just trying to get us with a more reliable suite.

Thanks for diving into it!

My vote would be disable this specific test for now and open an issue for it. Maybe someone will pick it up. We know the password change works and would surface pretty quickly from user feedback if it wasn't. And it seems like the only thing in question regarding the test is the redirect after updating, not the update itself.

@maebeale
Copy link
Copy Markdown
Collaborator Author

maebeale commented Apr 5, 2026

ok, @jmilljr24 i'm going to close this pr but leave the test in and see how flaky it actually is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flaky test: reset password person spec intermittently fails

2 participants