Setup cookie.signed in Rails 5 controller integration tests
Asked Answered
A

2

12

Imagine the scenario that there is a controller integration test calls a controller method, in which cookie.signed is used for some integrity checking.

Controller

# app/controllers/foo_controller.rb

def index
  entity = FooEntity.find_by_id(params[:id])
  if entity.nil?
    raise ActionController::BadRequest, 'Could not find requested entity.'
  else
    @is_authorized = entity.token == cookies.signed[:token]
    if @is_authorized
      # Success! The path to be tested.
    else
      raise ActionController::BadRequest, 'Unauthorized cookie token.'
    end
  end
end

Controller Test

# app/test/controllers/foo_test.rb

require 'test_helper'

class FooControllerTest < ActionDispatch::IntegrationTest
  test 'should be working' do
    cookies.signed[:token] = '7e5201169ef160e31058d2a1976a5552'
    get '/foobar/123'
  end
end

However, I'm not sure how to get cookie.signed setup in the test. The test code above throws an exception:

NoMethodError: undefined method `signed’ for Rack::Test::CookieJar:0x007fe90965ccd8

Tried to search for a solution, but the closest I could find was this article, https://sikac.hu/reconstruct-a-cookie-jar-and-read-signed-cookie-in-capybara-f71df387f9ff, but couldn't figure out how to construct ActionDispatch::Request object.

Autopilot answered 29/4, 2017 at 1:3 Comment(0)
A
39

This seems to be a known bug in Rails 5 and above (the linked issue is about cookies.encrypted but the same applies to cookies.signed). The problem is that in controller tests, the cookie jar is a Rack::Test::CookieJar class instance which does not support signed / encrypted cookies. On the other hand, in the application itself, the cookie jar is a ActionDispatch::Cookies::CookieJar class instance which supports both these special cookie types.

Nevertheless, to just construct a signed cookie in your controller test, you can manually create an ActionDispatch request cookie jar and use that to retrieve the signed cookie value:

# app/test/controllers/foo_test.rb

require 'test_helper'

class FooControllerTest < ActionDispatch::IntegrationTest
  test 'should be working' do
    my_cookies = ActionDispatch::Request.new(Rails.application.env_config.deep_dup).cookie_jar
    my_cookies.signed[:token] = '7e5201169ef160e31058d2a1976a5552'

    cookies[:token] = my_cookies[:token]
    get '/foobar/123'
  end
end

The first test line creates a new ActionDispatch request with the application requests default environment settings (they define e.g. the secret used for signing cookies) and returns it's cookie jar. Then you simply set the :token signed cookie for the desired value (this cookie jar does have the signed method defined as this is the ActionDispatch::Cookies::CookieJar, not Rack::Test::CookieJar). Finally, you retrieve the signed cookie value by accessing it without the signed accessor and set the same-named test cookie using this value.

Now, when the test reaches the controller code, the controller should see the proper value in the cookies.signed[:token] cookie.

Archfiend answered 29/4, 2017 at 19:29 Comment(4)
Works like a charm! Thanks for the detailed explanation. Much appreciated.Autopilot
Changing cookies will affect Rails.application.env_config, which will leak to other test cases. To avoid that, you can instead use protective copying: Rails.application.env_config.deep_dupDexedrine
A solution like this was exactly what I was looking for, but I can't for the life of me get it to work. No matter what I do, the session cookie cannot be parsed during the request.Rayner
You can use a test request (ActionDispatch::TestRequest.create) that will use the default env values with no need of duplicating them. api.rubyonrails.org/classes/ActionDispatch/…Legendary
G
0

This works for me:

## file: app/spec/controllers/api/v2/donations_controller_spec.rb

RSpec.describe Api::V2::DonationsController, type: :request do

 let(:donor) { Fabricate(:donor) }
  before do
     cookies = ActionDispatch::Request.new(Rails.application.env_config.deep_dup).cookie_jar
     @tmp = cookies.signed[:donor_jwtapi] = { value: { token: donor.token} , httponly: true }
 end

 describe 'POST#create' do
   it 'create a bank account donation' do
     post api_v2_donations_path, params: {donor_id: donor.id}, headers: { Cookie: "donor_jwtapi=#{@tmp[:value]};" }, as: :json

     json_response = JSON.parse response.body

     expect(response).to have_http_status :ok
     expect(json_response['error']).to be false
   end
  end
end
Goldoni answered 2/7, 2023 at 18:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.