Read more

RSpec: How to write isolated specs with cookies

Felix Eschey
August 30, 2023Software engineer at makandra GmbH

Background

Rails offers several methods to manage Show archive.org snapshot three types of different cookies along with a session storage for cookies Show archive.org snapshot . These are normal, signed and encrypted Show archive.org snapshot cookies.

Illustration web development

Do you need DevOps-experts?

Your development team has a full backlog? No time for infrastructure architecture? Our DevOps team is ready to support you!

  • We build reliable cloud solutions with Infrastructure as code
  • We are experts in security, Linux and databases
  • We support your dev team to perform
Read more Show archive.org snapshot

By following the happy path of testing a web application, that is only the main use-case is tested as a integration test and the rest as isolated (more unit like) tests, one might want to test some cookie dependent behavior as a request, helper or model spec too.

Since the rspec documentation Show archive.org snapshot on testing cookies is rather sparse and only focuses on controller specs, which recommended usage have been limited since Rails 5+ ( see "Rails: Support for Rails 5" Show archive.org snapshot ), this card will summarize some guidance on how to spec with cookies.

Warning

Note that using request.cookies and response.cookies is not recommend Show archive.org snapshot by RSpec anymore as can be read in the documentation link Show archive.org snapshot above.

Model specs

Whenever you have to use the same cookie dependent logic between controllers and some controller helpers, you should extract that into a class to DRY up your code and write solid unit tests.

Beware that your class will not be able to read the cookies, since they are only available within the controller and view context. Because of that, consider passing the required cookie value to the model initialization:

class Movie::FavoriteMovieCookies

  attr_reader :favorite_movie_ids

  def initialize(cookies, movie_id)
    @favorite_movies_cookie_value = cookies.signed[:favorite_movies]
    @movie_id = movie_id
    @cookies = cokies 
    
    set_favorite_movie_ids # read out from the cookie values
  end
  
   ... # some methods to parse and manipulate the cookies value
 end

Tip

Session cookies will do the parsing of JSON by default.

If it is not an option to stub the cookies jar object, you can create one by using:

cookies_jar = ActionDispatch::Cookies::CookieJar.build(nil, {})
cookies_jar.signed[:test] = 'test'

However #signed or #encrypted won't work within the model context on cookies_jar and will require stubbing these method or having to assure some of the cookies dependencies.

Helper Specs

Setting cookies in a helper spec Show archive.org snapshot shows that the cookies can simply set or expect by using one of the following options:

helper.cookies[:foo] = "bar"
helper.cookies.signed[:foo] = "bar"
helper.cookies.encrypted[:foo] = "bar"

Request Specs

In request specs you can simply access the current cookies by calling cookies. If you want to add a cookie before a request Show archive.org snapshot you can simply set the cookie on the cookies object as usual.

Signed and encrypted cookies

Even though the cookies method is available within the request spec, it is not working for signed and encrypted cookies.

Solution

One can simply decrypt the cookies by creating Show archive.org snapshot an object of ActionDispatch::Cookies::CookieJar. The reason for this is, that request specs use a Rack::Test::CookieJar object, while only ActionDispatch::Cookies::CookieJar supports the #encrypt and #signed methods.

If you do have to set an encrypted or signed cookie before the request you can use a ActionDispatch::Cookies::CookieJar object for this as well:

cookies_jar = ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash)
cookies_jar.signed[:user_id] = 1
cookes[:user_id] = cookies_jar[:user_id]

get "/movies/#{user_id}/favorite"

Example:

describe '/movies/:id/favorite' do
  def cookies_jar
    ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash)
  end

  it 'sets the cookies' do
    movie = create(:movie)
    user = sign_in(role: 'admin')
    
    patch "/movies/#{movie.id}/favorite"
    
    expect(cookies_jar.signed[:favorite_movies]).to eq "{\"#{user.id}\":[\"#{movie.id}\"]}"
  end
end
Felix Eschey
August 30, 2023Software engineer at makandra GmbH
Posted by Felix Eschey to makandra dev (2023-08-30 15:05)