devise icon indicating copy to clipboard operation
devise copied to clipboard

Devise in test environment: mapping.to.serialize_from_session gives error "wrong number of arguments (given 10, expected 2)"

Open dianedouglas-thrive opened this issue 1 year ago • 7 comments

Environment

  • Ruby [3.3.6]
  • Rails [8.0.0]
  • Devise [4.9.4]

Current behavior

I just updated my app to rails 8 with the newest version of Devise and hotwire turbo. When running tests with Capybara/Rspec I'm seeing certain form submissions fail with a 500 error from inside of Devise. This behavior seems to be only in the test environment.

lib/devise.rb:496

  # A method used internally to complete the setup of warden manager after routes are loaded.
  # See lib/devise/rails/routes.rb - ActionDispatch::Routing::RouteSet#finalize_with_devise!
  def self.configure_warden! #:nodoc:
    @@warden_configured ||= begin
      warden_config.failure_app   = Devise::Delegator.new
      warden_config.default_scope = Devise.default_scope
      warden_config.intercept_401 = false

      Devise.mappings.each_value do |mapping|
        warden_config.scope_defaults mapping.name, strategies: mapping.strategies

        warden_config.serialize_into_session(mapping.name) do |record|
          mapping.to.serialize_into_session(record)
        end

        warden_config.serialize_from_session(mapping.name) do |args|
          mapping.to.serialize_from_session(*args)
        end
      end

      @@warden_config_blocks.map { |block| block.call Devise.warden_config }
      true
    end
  end

When mapping.to.serialize_from_session(*args) is called I get the error "wrong number of arguments (given 10, expected 2)".

I have not been able to find a pattern between tests that pass and tests that cause this behavior except that I can see the data is different.

On a passing test args looks like this:

[1] pry(#<Warden::SessionSerializer>)>  args
=> [[1], "$2a$04$yPg9CBmwVeIZc3VYqdmL3O"]

But on a failing test args looks like this:

[1] pry(#<Warden::SessionSerializer>)> args
=> {"id"=>27,
 "email"=>"[email protected]",
 "created_at"=>"2000-01-01T05:00:03.000Z",
 "updated_at"=>"2000-01-01T05:00:19.000Z",
 "client_id"=>18,
 "role"=>"coordinator",
 "authentication_token"=>"pxcFTApxeyCo1evMNd9X",
 "first_name"=>"User12",
 "last_name"=>"Name",
 "analytics_enabled"=>false}

What could be causing this? Looks like a hash composed of a full user record instead of just an id and token.

I understand there have been issues with Devise.mapping with the newest version of rails, I've tried adding in the recommended initializer file from here, but it had no effect:

https://github.com/heartcombo/devise/pull/5728#issuecomment-2539418211

dianedouglas-thrive avatar Jan 07 '25 17:01 dianedouglas-thrive

It seems that while the test is running, devise hasn't configured its warden stuff, yet. That should be done while finalizing the route set: https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/lib/devise/rails/routes.rb#L19

Obviously the patch from https://github.com/heartcombo/devise/pull/5728 isn't enough to make it work in system tests.

If you want to use a quick workaround do something like that in your spec/spec_helper.rb:

RSpec.configure do |config|
  config.before(:each, type: :system) do
      Rails.application.try(:reload_routes_unless_loaded)
  end
end

aiomaster avatar Jan 20 '25 16:01 aiomaster

Also hurt by this. What's funny is that when I run the entire test suite it's all green. But when I do rspec spec/controllers/ANYTHING, the first controller test fails with that deserialization exception.

The workaround in previous comment works:

  config.before(:each, type: :controller) do
      Rails.application.try(:reload_routes_unless_loaded)
  end

Rails 8.0.1, Devise 4.9.4, Ruby 3.4.1.

tomash avatar Feb 18 '25 10:02 tomash

wow... spent hours troubleshooting that one.

indeed solves the issue.

benbonnet avatar May 25 '25 04:05 benbonnet

Have some issue with capibara

MeterSoft avatar Jun 17 '25 16:06 MeterSoft

AI has just put this in the code I'm working on (for the User model) and brought this issue to my attention

  # Override serialize_from_session to fix Rails 8/Devise 4.9.4 compatibility
  # See: https://github.com/heartcombo/devise/issues/5752
  def self.serialize_from_session(key, salt = nil)
    # Handle both old format [id, salt] and new format (full record hash)
    if key.is_a?(Hash)
      # New format: full record hash from Rails 8
      record_id = key["id"] || key[:id]
      return nil unless record_id
      find_by(id: record_id)
    elsif key.is_a?(Array) && key.length >= 1
      # Old format: [id] or [id, salt]
      record_id = key.first
      return nil unless record_id
      find_by(id: record_id)
    else
      # Single ID value
      find_by(id: key)
    end
  end

I'm yet to look into this but HTH

iRonin avatar Jul 22 '25 15:07 iRonin

This is the response that works. Thanks @aiomaster for taking the time to post this.

It seems that while the test is running, devise hasn't configured its warden stuff, yet. That should be done while finalizing the route set:

devise/lib/devise/rails/routes.rb

Line 19 in fec67f9

Devise.configure_warden! Obviously the patch from #5728 isn't enough to make it work in system tests.

If you want to use a quick workaround do something like that in your spec/spec_helper.rb:

RSpec.configure do |config| config.before(:each, type: :system) do Rails.application.try(:reload_routes_unless_loaded) end end

fieldse avatar Aug 08 '25 08:08 fieldse

As this is only a problem for the first spec, just running this before the suite also works and avoids any race conditions with other before blocks (which I ran into 😅) @aiomaster thanks so much for this fix, had flakey tests for a year or so 🥲

RSpec.configure do |config|
  config.before(:suite) do
      Rails.application.try(:reload_routes_unless_loaded)
  end
end

arenclissold avatar Aug 25 '25 04:08 arenclissold