Search code examples
ruby-on-railssessionrspecrspec-rails

Set Session in RSpec with Rails 7 API


I am trying to create a logic where a user gets an invitation from a group. If they signup via that invitation, the group id will be stored in the session and when they signup, they will be automatically added to the group as a member.

I am using Devise for authentication. So I customized it like this:

class Users::RegistrationsController < Devise::RegistrationsController
  def create
    super
    if @user
      # We create a membership to the group if the user had it in the session (it means that it was invited)
      Membership.create(user: @user, group: session[:group]) if session[:group]
      # We remove the existing session
      reset_session
    end
  end
end

Now, I want to test this behaviour. I would like to test 2 scenarios:

  1. User doesn't have the session and signs up just like that
  2. User has the session and signs up -> a group membership is generated

The first one is fine but the second one is the one I am struggling at:

context "when user signs up via invitation" do
 let(:group) {create :group}

 before do
  # We need to create an icon so that we can attach it to the use when created
  create :icon
  
  post user_registration_path,
  params: { "user": { "username": "test", "email": "[email protected]", "password": "mypassword" }},
  session: { "group": group.id }
 end

 it "has a session saved with group id" do
  expect(session[:group]).to eq group.id
 end
end

I found this way here, but it throws the following error:

ArgumentError:
       unknown keyword: :session

If I try making the call like this instead:

params: { user: { "username": "test", "email": "[email protected]", "password": "mypassword" }, session: { "group": group.id}}

It still throws an error:

NoMethodError:
       undefined method `enabled?' for {}:Hash
     
               return unless session.enabled?

I also tried setting it like this:

request.session[:group] = group.id

After I make the post call (with only the params). It does pass the expectation but I cannot fetch it from the controller.

Also, just setting it like this:

session[:group] = group.id

Throws the following error:

NoMethodError:
       undefined method `session' for nil:NilClass
     
             @request.session

Finally, if I try to mock it inside the before {} block:

allow_any_instance_of(ActionDispatch::Request).to receive(:session).and_return( group: group.id )

It gives me the following error:

NoMethodError:
       undefined method `enabled?' for {:group=>1}:Hash
     
               return unless session.enabled?

Any ideas on how can I tackle this?

PD. Using Rails 7 API and ruby 3.1.2

and also in my application.rb I added the following to be able to work with sessions in the app (and when I test manually it does work)

# Configure session storage
config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies
config.middleware.use config.session_store, config.session_options

Thanks!


Solution

  • I also struggled with this until I followed this approach and included a "session double" support, like this example:

    Support:

    https://github.com/DFE-Digital/schools-experience/blob/master/spec/support/session_double.rb

    Usage:

    https://github.com/DFE-Digital/schools-experience/blob/master/spec/controllers/schools/sessions_controller_spec.rb

    Your Example

    # your test...
    
    RSpec.describe '...', type: :request do
      describe '...' do
        include_context 'session double'
    
        let(:group) {create :group}
        let(:session_hash) { {} }
    
        before do
          session_hash[:group] = group.id
        end
    
        it 'has a session saved with group id' do
          post user_registration_path, params: { "user": { "username": "test", "email": "[email protected]", "password": "mypassword" }}
    
          expect(session[:group]).to eq group.id
        end
      end
    end
    

    Add Session Support:

    # spec/support/session_double.rb
    
    shared_context 'session double' do
      let(:session_hash) { {} }
    
      before do
        session_double = instance_double(ActionDispatch::Request::Session, enabled?: true, loaded?: false)
    
        allow(session_double).to receive(:[]) do |key|
          session_hash[key]
        end
    
        allow(session_double).to receive(:[]=) do |key, value|
          session_hash[key] = value
        end
    
        allow(session_double).to receive(:delete) do |key|
          session_hash.delete(key)
        end
    
        allow(session_double).to receive(:clear) do |_key|
          session_hash.clear
        end
    
        allow(session_double).to receive(:fetch) do |key|
          session_hash.fetch(key)
        end
    
        allow(session_double).to receive(:key?) do |key|
          session_hash.key?(key)
        end
    
        allow_any_instance_of(ActionDispatch::Request)
          .to receive(:session).and_return(session_double)
      end
    end