23
votes

I'm trying to set the header for some RSpec requests that require authentication. The header is ACCESS_TOKEN. No matter how I attempt to set the header, it never gets set. I know the app works because I can manually test it, I just cant get rspec tests to work. See the full source code & tests for this problem here: https://github.com/lightswitch05/rspec-set-header-example

Since authentication is used in most of my request specs, I've created support helper module to retrieve an access token and set it in the header. Below is the summary of how I'm trying to set the header, see everything I've tried in the full source

# my_app/spec/support/session_helper.rb
module SessionHelper
  def retrieve_access_token
    post api_v1_session_path({email: '[email protected]', password: 'poor_password'})

    expect(response.response_code).to eq 201
    expect(response.body).to match(/"access_token":".{20}"/)
    parsed = JSON(response.body)
    token = parsed['access_token']['access_token']

    @request.headers['HTTP_ACCESS_TOKEN'] = token
  end
end

an example request spec that uses this helper and should work, but always fails because the header never gets set:

# my_app/spec/requests/posts_spec.rb
# ...
context "create" do
  it "creates a post" do
    retrieve_access_token
    post = FactoryGirl.build(:post)

    post api_v1_posts_path(
      post: {
        title: post.title,
        content: post.content
      }
    )

    expect(response.body).to include('"id":')
    expect(response.body).to include('"title":"' + post.title + '"')
    expect(response.body).to include('"content":"' + post.content + '"')
    expect(response.response_code).to eq 201
  end
end

I know I can manually set the header in the individual get and post requests - but that is not a maintainable solution for API-wide authorization. Imagine having to change every test if the header name changed slightly.

5

5 Answers

41
votes

Note: This answer is based on what you seem to be calling api_v1_session_path with post request to SessionsController for every spec you're trying to run in your requests specs.

There are two ways to solve the issue I figured you have here.

Solution #1 - Either you create another helper method in your SessionHelper or in some other helper file called support/requests_helper.rb(however you prefer). I'd create another helper in support/requests_helper.rb:

module RequestsHelper
  def get_with_token(path, params={}, headers={})
    headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
    get path, params, headers
  end

  def post_with_token(path, params={}, headers={})
    headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
    post path, params, headers
  end

  # similarly for xhr..
end

then in rails_helper.rb:

  # Include the sessions helper
  config.include SessionHelper, type: :request
  # Include the requests helper
  config.include RequestsHelper, type: :request

change session_helper.rb:

# my_app/spec/support/session_helper.rb
module SessionHelper
  def retrieve_access_token
    post api_v1_session_path({email: '[email protected]', password: 'poor_password'})

    expect(response.response_code).to eq 201
    expect(response.body).to match(/"access_token":".{20}"/)
    parsed = JSON(response.body)
    parsed['access_token']['access_token'] # return token here!!
  end
end

Now, you can change your all requests specs like this:

describe Api::V1::PostsController do

  context "index" do
    it "retrieves the posts" do
      get_with_token api_v1_posts_path

      expect(response.body).to include('"posts":[]')
      expect(response.response_code).to eq 200
    end

    it "requires a valid session key" do
      get api_v1_posts_path

      expect(response.body).to include('"error":"unauthenticated"')
      expect(response.response_code).to eq 401
    end
  end
end

Solution #2 - Change specs/factories/access_token_factory.rb to:

FactoryGirl.define do
  factory :access_token do
    active true
  end

  # can be used when you want to test against expired access tokens:
  factory :inactive_access_token do
    active false
  end
end

Now, change your all requests specs to use access_token:

describe Api::V1::PostsController do

  context "index" do
    let(:access_token){ FactoryGirl.create(:access_token) }

    it "retrieves the posts" do
      # You will have to send HEADERS while making request like this:
      get api_v1_posts_path, nil, { 'HTTP_ACCESS_TOKEN' => access_token.access_token }

      expect(response.body).to include('"posts":[]')
      expect(response.response_code).to eq 200
    end

    it "requires a valid session key" do
      get api_v1_posts_path

      expect(response.body).to include('"error":"unauthenticated"')
      expect(response.response_code).to eq 401
    end
  end
end

I'd go with "Solution #1" as it removes a burden of making you remember to send HTTP_ACCESS_TOKEN in headers every time you want to make such requests.

14
votes

Common misconception is to treat controller and request tests equally.

It would be good to start from reading about controller specs and request specs. As you can see, controller specs simulate http request, while request specs perform full stack request.

You can find some good article about why you should write controller specs and what to test there here. While it is good to write them, they shouldn't be touching database in my opinion.

So while Voxdei answer is partially valid (after changing request specs to controller specs your way of setting headers will work), it misses the point in my opinion.

In request specs, you cannot just use request / controller methods, you have to pass your headers in hash as third argument of your request methods, so i.e.

post '/something', {}, {'MY-HEADER' => 'value'}

What you could do though is to stub authentication like:

before do
  allow(AccessToken).to receive("authenticate").and_return(true)
end

Then you could test your authentication in one spec to be sure that it works and use such before filter in other specs. This is also probably better approach as performing additional request every time you run spec needing authentication is quite huge overhead.

I also found quite interesting pull request in grape gem which tries to add default headers behaviour so you could also try with such approach if you would really want to use default headers in request specs.

4
votes

Probably because of how now Rspec treats spec files. It no longer automatically infers spec type from a file location

Try either setting this behavior back to what you used to know

RSpec.configure do |config|
  config.infer_spec_type_from_file_location!
end

or set it locally for each controller spec files in your project

describe MyController, type: :controller do
  # your specs accessing @request
end
2
votes

Surya's answer is the best. But you can DRY it up a little bit more:

def request_with_user_session(method, path, params={}, headers={})
    headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
    send(method, path, params, headers)
end

Here you have only one method and call the request method by the given parameter method.

0
votes

I stub the function that authenticates the request to return true or any value returned by the function.

ApplicationController.any_instance.stub(:authenticate_request) { true }