The setup is the following. For each http request the manager sends his credentials in the header(name,pw). These get checked against the entries in the db and if they succeed return the desired user object. How is it possible to implement basic http_auth in the request tests? I would like to add only the password and username and test the return value? Which is the goal of request tests,right? I tried the following without much success:
I created an AuthHelper module in spec/support/auth_helper.rb with
module AuthHelper
def http_login
user = 'test'
pw = 'test'
request.ENV['HTTP_AUTHORIZATION'] =ActionController::HttpAuthentication::Basic.encode_credentials(user,pw)
end
end
and use it in the requests/api/user_spec.rb as follows
include AuthHelper
describe "User API get 1 user object" do
before(:each) do
http_login
end
but i receive this error message. How can i fix this and enable my tests to pass http_auth? I read lot of similar topis and questions also here but
they apply mostly to older versions of rspec and rails and are not applying to my case
Thanks in advance!
Failure/Error: request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user,pw)
NoMethodError:
undefined method `env' for nil:NilClass# ./spec/support
# ./spec/support/auth_helper.rb:5:in `http_login'
# ./spec/requests/api/user_spec.rb:8:in `block (2 levels) in <top (required)>'
Update
I moved the header generation inside a request. I looked up the Auth verb, so i think the assignment should work. I tested the ActionController call in rails console and received a string. I suppose this is also correct.My whole test now looks like this:
describe "User API get 1 user object", type: :request do
it 'Get sends back one User object ' do
headers = {
"AUTHORIZATION" =>ActionController::HttpAuthentication::Basic.encode_credentials("test","test")
# "AUTHORIZATION" =>ActionController::HttpAuthentication::Token.encode_credentials("test","test")
}
FactoryGirl.create(:user)
get "/api/1/user", headers
#json = JSON.parse(response.body)
expect(response).to be_success
# expect(response.content_type).to eq("application/json")
end
end
receiving the following error:
which incudles the line #buf=["HTTP Basic: Access denied.\n"] so access is still denied.
Failure/Error: expect(response).to be_success
expected `#<ActionDispatch::TestResponse:0x000000070d1d38 #mon_owner=nil, #mon_count=0, #mon_mutex=#<Thread::Mutex:0x000000070d1c98>, #stream=#<ActionDispatch::Response::Buffer:0x000000070d1c48 #response=#<ActionDispatch::TestResponse:0x000000070d1d38 ...>,
#buf=["HTTP Basic: Access denied.\n"], #closed=false>, #header={"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1; mode=block", "X-Content-Type-Options"=>"nosniff", "WWW-Authenticate"=>"Basic realm=\"Application\"", "Content-Type"=>"text/html; charset=utf-8", "Cache-Control"=>"no-cache", "X-Request-Id"=>"9c27d4e9-84c0-4ef3-82ed-cccfb19876a0", "X-Runtime"=>"0.134230", "Content-Length"=>"27"}, #status=401, #sending_file=false, #blank=false,
#cv=#<MonitorMixin::ConditionVariable:0x000000070d1bf8 #monitor=#<ActionDispatch::TestResponse:0x000000070d1d38 ...>, #cond=#<Thread::ConditionVariable:0x000000070d1bd0>>, #committed=false, #sending=false, #sent=false, #content_type=#<Mime::Type:0x00000002af78f8 #synonyms=["application/xhtml+xml"], #symbol=:html, #string="text/html">, #charset="utf-8", #cache_control={:no_cache=>true}, #etag=nil>.success?`
to return true, got false
SOLUTION
This test is not polished (yet) but at least it passes now.
describe "User API get 1 user object", type: :request do
it 'Get sends back one User object ' do
#env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user,pw)
FactoryGirl.create(:user)
get "/api/1/user", {}, #env
JSON.parse(response.body)
expect(response).to be_success
expect(response.status).to eq 200
end
end
Read the error carefully: undefined method `env' for nil:NilClass means request is nil. Are you trying to set the header before a test while you are defining the request later on in the test?
You might want to look at the documentation for an example on how to set headers.
If you're still stuck, post one of your tests as well.
This line looks suspicious:
request.ENV['HTTP_AUTHORIZATION'] =ActionController::HttpAuthentication::Basic.encode_credentials(user,pw)
Are you sure that "ENV" should be capitalized? I think it should be written like "env".
Related
I have the following code in my API file
class TransactionStatus < Grape::API
helpers ::PushAPIv1::NamedParams
post '/transaction/status' do
Rails.logger.warn "#{params.to_xml}"
// some piece of code
end
end
I tried to write Rpsec for this condition but not getting any success in my coverage report. The Spec I tried to write is as below
require 'rails_helper'
RSpec.describe Transaction, type: :request do
context 'check Transaction Status' do
it 'should log an info message' do
expect(Rails.logger).to receive(:warn).with("#{params.to_xml}")
end
it 'should raise Transaction Not Found Error if invalid transaction' do
transaction = FactoryGirl.build(:transaction, state: 'processing', gateway_message: 'success', ref_code: '1qqqq1')
p transaction.ref_code
expect { Transaction.find_by_ref_code('q1111q').should eq transaction }.to raise_error()
end
end
end
Well, if what you're trying to achieve is coverage of your POST /transaction/status endpoint, then... you need to reach for the endpoint in your specs, which you're not doing at the moment.
it 'should log an info message' do
expect(Rails.logger).to receive(:warn).with("#{params.to_xml}")
end
Here you expect Rails.logger to receive warn message. But you need to trigger something that should call Rails.logger.warn for the spec to pass.
it 'should raise Transaction Not Found Error if invalid transaction' do
transaction = FactoryGirl.build(:transaction, state: 'processing', gateway_message: 'success', ref_code: '1qqqq1')
expect { Transaction.find_by_ref_code('q1111q').should eq transaction }.to raise_error()
end
About this spec: you're mixing expect and should syntaxes in a manner that's hardly understandable. Plus you're just using ActiveRecord methods, and never call your actual API endpoint. That's why you don't get any code coverage.
In the end, what you should do to get proper coverage of your endpoint is actually calling it. This could be done in a before :each block for instance, or even in each of your spec, like this:
describe 'transaction/status' do
before :each do
post 'path/to/api/transaction/status'
# post 'path/to/api/transaction/status', params: { some: params }
# post 'path/to/api/transaction/status', headers: { some: headers }
end
it '...' do
expect( response ).to ...
end
end
You get the idea. You can check out RSpec Rails 3.7 - Request specs for more details and examples.
I am new to RSpec but here I am trying to create tests based on this code and I am keep on getting this error. Any suggestions?
CODE:
serialization_scope nil
before_action :set_list, only: [:show, :destroy, :update]
before_action :verify_user, only: :show
def create
#list = current_user.lists.build(list_params)
if #list.save
render json: {message: ['Success']}, status: 200
else
render json: {errors:[#list.errors.full_messages]}, status: 400
end
end
Here is the RSpec file that I started :
require "rails_helper"
RSpec.describe V1::ListsController, :type => :controller do
describe "POST create" do
it "returns HTTP status" do
expect(post :create).to change(#list, :count).by(+1)
expect(response).to have_http_status :success #200
end
end
describe 'GET status if its not created' do
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
expect(response.status).to eq 400
end
end
end
And the error that I got is :
Failures:
1) V1::ListsController GET status if its created returns HTTP status
Failure/Error: expect(post :create).to change(#list, :count).by(+1)
expected #count to have changed by 1, but was not given a block
# ./spec/controllers/lists_controller_spec.rb:8:in `block (3 levels) in <top (required)>'
2) GET status if its not created return HTTP status - reports BAD REQUEST (HTTP status 400)
Failure/Error: expect(response.status).to eq 400
expected: 400
got: 200
(compared using ==)
Try this code.
require 'rails_helper'
RSpec.describe V1::ListsController, type: :request do
describe 'valid request' do
it 'returns HTTP status' do
post '/list', params: { list: { list_name: 'xyz' } }
expect(response.status).to eq 201
end
end
describe 'invalid request' do
it "should return unauthorized" do
post '/list'
assert_response :unauthorized
end
end
end
In params you need to pass your list_params.
Spec would look like:
describe "POST create" do
context 'valid request' do
it 'should increase #list item' do
expect { post :create }.to change(List, :count).by(1)
end
it "returns HTTP status" do
post :create
expect(response).to have_http_status :success #200
end
end
context 'invalid request' do
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
get :create
expect(response.status).to eq 400
end
end
end
Cheers!
You can test an object not being created by intentionally causing some of its validations to fail e.g. you can pass a mandatory attribute as nil from the RSpec.
Sample request: post :create, { title: nil }.
But as per your RSpec code, it seems there are no validations on List model. So, lets try to stub save and return false for this particular test.
describe 'GET status if its not created' do
# Assuming your model name is `List`
before { allow_any_instance_of(List).to receive(:save) { false } }
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
post :create
expect(response.status).to eq 400
end
end
Please post your model for list and i can update the answer with more appropriate test.
Ishika, let me see if I can help you :)
RSpec official documentation recommends you to use request specs instead of controller specs. That is recommended because Rails 5 deprecated some methods used on controller testings. You can read more about this here at RSpec blog
ps.: You can use controller tests so far, but it can be deprecated in a future major version of RSpec.
There are some notes I left after the code, please read them also.
I would write a request spec like this:
# spec/requests/v1/lists_controller_create_spec.rb
require "rails_helper"
RSpec.describe V1::ListsController do
describe 'success' do
it 'returns ok and creates a list', :aggregate_failures do # :aggregate_failures is available only for RSpec 3.3+
expect do
post '/list', title: 'foo' # This will also test your route, avoiding routing specs to be necessary
end.to change { List.count }.from(0).to(1)
expect(response).to have_http_status(:ok)
end
end
describe 'bad request' do
before do
# This is needed because your controller is not validating the object, but look at my
# comment below (out of the code), to think about this behavior, please.
allow_any_instance_of(List).to receive(:save).and_return(false)
end
it 'returns a bad request and does not create a list' do
expect do
post '/list', title: 'foo' # This will also test your route, avoiding routing specs to be necessary
end.not_to change { List.count }
expect(response).to have_http_status(:bad_request)
end
end
end
Notes:
I suggested using more than 1 expectation by example, that is ok in this spec because they are simple and because I'm using :aggregate_failures option. With this option, if the first expectation fails, the next expectations will also be executed, considering that in this case, the following expectations does not depend on the first one, it is ok to use more than 1 expectation for the example.Reference
You are returning a bad request if the object is not saved, but you are not validating it. If your model has validations that will validate the object there, please adjust the specs to fail the save (instead of using the mock I used) and consider rendering an error message in the response
If you think that making the post inside a expect block, you can do different: Store the count of Lists in a variable before making the post and after the post you test if the variable has changed or not, maybe you think it will be more clear and it will do exactly the same thing in the background.
I am trying to test my Rails application using RSpec, but my tests are failing because RSpec seems to not be passing the headers I give it to Rails.
I have a UsersController that includes ApplicationHelper, and in ApplicationHelper I have a method that accesses the headers hash. Indexing it by my SESSION_KEY header returns nil. If I puts headers inside that method, the hash does not contain the header I have supplied, only the following: {"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1; mode=block", "X-Content-Type-Options"=>"nosniff"}.
Here is the relevant part of my RSpec spec:
require 'rails_helper'
describe Api::V1::UsersController, type: :request do
let(:user) { User.create(name: 'TestUser', email: 'someone#example.com', password: 'password123', password_confirmation: 'password123') }
let(:id) { user.id }
let(:sess) { user.sessions.create }
before { get "/api/v1/users/#{id}" }
# Snipped other tests
context 'with authentication' do
context 'with a valid id' do
it 'returns full user information' do
get "/api/v1/users/#{id}", nil, {'HTTP_SESSION_KEY': sess.key}
response_user = response_json[:user]
expect(response.status).to eq 200
expect(response_user).to_not be_nil
expect(response_user[:name]).to eq user[:name]
expect(response_user[:email]).to eq user[:email]
end
end
end
def response_json
JSON.parse(response.body, symbolize_names: true)
end
end
I have also tried passing the SESSION_KEY header without HTTP_ before it, and that did not work. I have also tried moving it up to the top get in the before block to see if it was a context issue, and that did not work either.
Docs say the above should work, but if for some reason rspec is interpreting your test as a :controller test and not a :request test then you need to do this (just before your get call):
request.env["HTTP_SESSION_KEY"] = sess.key
Below is my rspec. Notice that three lines are commented out. This rspec passes.
describe "/my_api_endpoint" do
it 'Blah Blah Blah' do
post "/my_api_endpoint", #params
# expect(response).to be_success
# json = JSON.parse(response.body)
# puts "json = #{json}"
last_response.should be_ok
end
end
However, not only do I need to know that the POST returned success, I also need to examine the JSON it returns. Somehow I cannot do this. When I comment those three lines back in, I get the following error
NameError: undefined local variable or method `response' for <RSpec::Core::ExampleGroup::Nested_1::Nested_9:0x007f84c58c31f0>`
How do I examine the results of the post?
My work colleague provided me this answer:
For Sinatra apps the response is in last_response not in response.
This is an adopted rails app with no tests. I am trying to test omniauth in an integration test but am getting an error (edit I have based upon this: https://github.com/intridea/omniauth/wiki/Integration-Testing). This reflects my lack of understanding of Rspec. It would seem that the request object would be available by default.
I have in my spec/spec_helper.rb:
config.include IntegrationSpecHelper, :type => :request
Capybara.default_host = 'http://localhost:3000'
OmniAuth.config.test_mode = true
OmniAuth.config.add_mock(:facebook, {
:uid => '12345'
})
and in my spec/integration/login_spec:
require 'spec_helper'
describe ServicesController, "OmniAuth" do
before do
puts OmniAuth.config.mock_auth[:facebook]
puts request # comes back blank
request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:facebook]
end
it "sets a session variable to the OmniAuth auth hash" do
request.env["omniauth.auth"][:uid].should == '12345'
end
end
and I get the following error:
{"provider"=>"facebook", "uid"=>"12345", "user_info"=>{"name"=>"Bob
Example"}}
F
Failures:
1) ServicesController OmniAuth sets a session variable to the
OmniAuth auth hash
Failure/Error: request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:facebook]
NoMethodError:
undefined method env' for nil:NilClass
# ./login_spec.rb:8:inblock (2 levels) in '
Finished in 22.06 seconds 1 example, 1 failure
Failed examples:
rspec ./login_spec.rb:11 # ServicesController OmniAuth sets a session
variable to the OmniAuth auth hash
Should the request object be available here, by default? Does this error possibly mean something else?
thx
You're getting nil because you haven't made any request yet.
To make the test work, you have to do three things:
Set up the mock
Make the request
Test whatever code is attached to the callback
Here's how I do it. First set up the mock in the before block, and then visit the URL corresponding to the provider (in this case facebook):
before do
OmniAuth.config.add_mock(:facebook, {:uid => '12345'})
visit '/auth/facebook'
end
From the wiki:
A request to /auth/provider will redirect immediately to /auth/provider/callback.
So you have to have a route which matches '/auth/:provider/callback'. Whatever action you map that do has to perform the stuff in step 3 above.
If you wanted to test that the session variable was set to the uid, you could do something like this (which works because you set the uid to '12345' in the mock above):
it "sets a session variable to the OmniAuth auth hash" do
session['uid'].should == '12345'
end
And here's a route and action that should make this pass:
routes.rb
match '/auth/:provider/callback' => 'sessions#callback'
controllers/sessions_controller.rb
def callback
session['uid'] = request.env["omniauth.auth"][:uid]
end
That's the gist of it. Hope that helps.