问题
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: 'test@example.com', 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.
回答1:
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: 'test@example.com', 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.
回答2:
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.
回答3:
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
回答4:
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
.
回答5:
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 }
来源:https://stackoverflow.com/questions/25815741/set-header-in-rspec-3-request