Devise: 4.20
Rails: 5.0.1
Rspec: 3.5
I had used this link https://github.com/plataformatec/devise/wiki/How-To:-Use-HTTP-Basic-Authentication, but I havproblems to test the http basic auth for my api using rspec for requests tests. Below is the example the error:
app/controllers/api/base_controller.rb
module Api
class BaseController < ApplicationController
before_action :authenticate_user!
protected
def authenticate_user!
authenticate_or_request_with_http_basic do |username, password|
resource = User.find_by_username(username)
if resource
sign_in :user, resource if resource.valid_password?(password)
else
request_http_basic_authentication
end
end
end
end
end
app/controllers/api/v1/car_controller.rb
module Api
module V1
class CarController < Api::BaseController
respond_to :json
def index
#cars = Car.all
render :json => {:content => #cars}, :status => 200
end
end
end
end
spec/requests/api/v1/car_controller_spec.rb
require 'rails_helper'
RSpec.describe "Servers API", :type => :request do
it 'sends a list of servers' do
admin = FactoryGirl.create(:admin)
#env = {}
#env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(admin.username, admin.password)
get "/api/cars", :params => {}, :headers => #env
# test for the 200 status-code
expect(response.status).to eq(200)
end
end
When I run the spec, I have the below error:
# --- Caused by: ---
# NoMethodError:
# undefined method `sign_in' for #<Api::V1::CarController:0x0000000609ef12>
# ./app/controllers/api/base_controller.rb:10 in block in authenticate_user!
Anyone can help me? Thanks.
I have similar setup where my specs are passing, would you also show your spec_helper content, looks like you are not including Devise::TestHelpers.
spec_helper
RSpec.configure do |config|
config.include Devise::TestHelpers
config.include Warden::Test::Helpers
config.before { Warden.test_mode! }
config.after { Warden.test_reset! }
config.before(:each) do
#headers = { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
end
and my test looks something like this:
RSpec.describe 'Users' do
context 'when client is authorized' do
let(:user) { FactoryGirl.create(:user) }
it 'gets user' do
#headers['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.
encode_credentials(
user.authentication_token,
email: user.email
)
get api_v1_user_url(id: user.id), {}, #headers
expect(response.status).to eq(200)
end
end
end
Related
I have a controller concern defined as follows:
module Auth
extend ActiveSupport::Concern
private
def sign_in(user)
session[:customer_id] = user.id
self.current_user = user
end
def signed_in?
!current_user.nil?
end
...
# other methods
end
And this module is included in the ApplicationController:
class ApplicationController < ActionController::Base
include Auth
helper_method :signed_in?, :current_user, :current_user?
before_action :authorize
end
The questions I have:
should we really extend extend ActiveSupport::Concern in a concern? If so wy and when not?
how can I enable the concern module so that its methods could be re-used in RSpec request specs?
I tried to add the following in spec/support/utilities.rb:
include ApplicationHelper
include Auth
as well as required all the files from spec/support directory in rails_helper.rb and included the above modules:
# Require all the files in support directory
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
RSpec.configure do |config|
...
# include ApplicationHelper
config.include ApplicationHelper, type: :feature
config.include Auth, type: :request
...
end
Now, when running a requests/customers_spec.rb:
require 'rails_helper'
RSpec.describe "/customers", type: :request do
describe "GET /index" do
it "renders a successful response" do
customer = create(:customer)
sign_in(customer)
get customers_url
expect(response).to be_successful
end
end
...
it fails with:
Failures:
1) /customers GET /index renders a successful response
Failure/Error: session[:customer_id] = user.id
NoMethodError:
undefined method `session' for nil:NilClass
# ./app/controllers/concerns/auth.rb:7:in `sign_in'
# ./spec/requests/customers_spec.rb:10:in `block (3 levels) in <top (required)>'
What am I missing?
I'm using Rails 6.0.3, rspec-rails 4.0.1.
The solution I came to was to add the following method to the spec/support/login_macros.rb:
module LoginMacros
def sign_in_with_browser(user)
visit login_path
fill_in 'E-mail', with: user.email
fill_in 'Password', with: user.password
click_button 'Login'
end
def sign_out_with_browser
click_link 'Logout'
end
# to enable user authentication in request specs
def login_without_browser(user)
post login_url, params: { email: user.email, password: 'secret' }
end
end
Then test a protected end-point in the above request spec as follows:
describe "GET /index" do
it "renders a successful response" do
customer = create(:customer)
login_without_browser(customer)
get customers_url
expect(response).to be_successful
end
end
I run my test but the sign_in don't authenticate.
require 'rails_helper'
describe "get all contacts route", :type => :request do
let!(:contacts) {FactoryBot.create_list(:random_contacts, 20)}
before(:each) do
get '/api/v1/contacts'
user = FactoryBot.create(:user)
request.env["devise.mapping"] = Devise.mappings[:user]
request.headers['X-User-Email'] = "#{user.email}"
request.headers['X-User-Token'] = "#{user.authentication_token}"
sign_in user
end
it 'returns all contacts' do
expect(JSON.parse(response.body).size).to eq(20)
end
it 'returns status code 200' do
expect(response).to have_http_status(:success)
end
end
The return of the test is:
"{\"error\":\"You need to sign in or sign up before continuing.\"}"
https://github.com/andbri321/one_bit_contacts/tree/feature/test_contact
You might want to create some sign in/out helpers. Something along the lines of
module DeviseRequestSpecHelpers
include Warden::Test::Helpers
def sign_in(resource_or_scope, resource = nil)
resource ||= resource_or_scope
scope = Devise::Mapping.find_scope!(resource_or_scope)
login_as(resource, scope: scope)
end
def sign_out(resource_or_scope)
scope = Devise::Mapping.find_scope!(resource_or_scope)
logout(scope)
end
end
And then load them in your RSpec config like so:
RSpec.configure do |config|
config.include DeviseRequestSpecHelpers, type: :request
end
Then use these helpers before the tests are run.
Look here for more info.
Im writing some tests for my controller, but im getting the error below when i'm running rspec ./spec/controllers, when i just run the spec direct in the file i'm getting everything green.
1) VersionOne::UsersController GET #actives should return all actives
Failure/Error: get :actives
ActionController::UrlGenerationError:
No route matches {:action=>"actives", :controller=>"version_one/users"}
# ./spec/controllers/version_one/users_controller_spec.rb:13:in `block (3 levels) in <top (required)>'
2) VersionOne::UsersController GET #archived should return all archives
Failure/Error: get :archives
ActionController::UrlGenerationError:
No route matches {:action=>"archives", :controller=>"version_one/users"}
# ./spec/controllers/version_one/users_controller_spec.rb:23:in `block (3 levels) in <top (required)>'
3) VersionOne::UsersController POST #create should create a new collaborator with success
Failure/Error: post :create, params: params
This is my controller
class UsersController < ApiControler
def actives
users = User.actives
render json: users
end
def archives
users = User.archiveds
render json: users
end
def create
user = User.build(user_params)
render json: user
end
end
This are my routes
scope 'api' do
scope 'v1', module: 'version_one' do
resources 'users' do
collection do
get 'actives'
get 'archives'
end
member do
match 'active'
match 'archive'
end
end
end
end
This are my tests
RSpec.describe VersionOne::UsersController, type: :controller do
before(:all) do
7.times { Collaborator.build(attributes_for(:user)) }
8.times { Admin.build(attributes_for(:user)) }
5.times { Collaborator.build(attributes_for(:user)).archived! }
end
describe "GET #actives" do
it "should return all actives" do
get :actives
body = JSON.parse(response.body)
expect(response).to have_http_status(:success)
expect(body["pagination"]["per_page"]).to eq(20)
expect(body["pagination"]["total_objects"]).to eq(15)
end
end
describe "GET #archived" do
it "should return all archives" do
get :archives
body = JSON.parse(response.body)
expect(response).to have_http_status(:success)
expect(body["pagination"]["per_page"]).to eq(20)
expect(body["pagination"]["total_objects"]).to eq(5)
end
end
describe "POST #create" do
it "should create a new collaborator with success" do
params = { kind: "Collaborator", user: { name: "User", email: "user#email.net", password: "123456", cpf: "36156291830" } }
post :create, params: params
body = JSON.parse(response.body)
expect(response).to have_http_status(:success)
expect(body).to include(
"name" => "User",
"cpf" => "36156291830",
"email" => "user#email.net",
)
end
end
end
this is my .rspec
--color
--format documentation
--require rails_helper
this is my rails_helper
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'spec_helper'
require 'rspec/rails'
require 'shoulda-matchers'
require 'rack/test'
require 'faker'
require 'rake'
ActiveRecord::Migration.maintain_test_schema!
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
config.include Shoulda::Matchers::ActiveModel, type: :model
config.include Shoulda::Matchers::ActiveRecord, type: :model
config.fixture_path = "#{::Rails.root}/spec/factories"
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.clean_with(:truncation)
end
config.use_transactional_fixtures = false
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
end
am'i forgetting something?
You do not have namespaces for UsersController
try:
module Api
module VersionOne
class UsersController < ApiController
# your code
end
end
end
You may also need to change ApiController to Api::VersionOne::ApiController
I have a rails-api application that I'm testing using Rspec. The Application uses devise_token_auth gem for authentication and cancancan gem for authorization.
devise_token_auth requires that the client include these authentication headers in every request:
access-token, client, expiry, uid. These headers are available in a response after successful authentication using email and password.
I have decided to use a solution provided in this answer to set these headers during testing.
In ability.rb I have this:
## models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
if user.role? :registered
can :create, Post, user_id: user.id
can :update, Post, user_id: user.id
can :destroy, Post, user_id: user.id
can :read, Post
end
end
end
posts#show action in PostsController looks like this:
## controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate_user!
load_and_authorize_resource
def show
render json: #post
end
end
I have rescued CanCan::AccessDenied error to render a json message and a status of 403 forbidden in ApplicationController
rescue_from CanCan::AccessDenied do |exception|
render json: {"message" => "unauthorized"}.to_json, :status => 403
end
I have this in spec/support/session_helper.rb
module SessionHelper
def retrieve_access_headers
##I have a user with these credentials and has a "registered" role. in the the test db.
post "/auth/sign_in", params: {:email => "registered_user#gmail.com", :password => "g00dP#ssword"}, headers: {'HTTP_ACCEPT' => "application/json"}
##These two pass
expect(response.response_code).to eq 200
expect(response.body).to match(/"email": "registered_user#gmail.com"/)
access_headers = {"access-token" => response.headers["access-token"],
"client" => response.headers["client"],
"expiry" => response.headers["expiry"],
"uid" => response.headers["uid"],
"token-type" => response.headers["token-type"],
'HTTP_ACCEPT' => "application/json"
}
return access_headers
end
end
I have this in spec/support/requests_helper.rb
module RequestsHelper
def get_with_token(path, params={}, headers={})
headers.merge!(retrieve_access_headers)
get path, params: params, headers: headers
#### this outputs the expected headers on a json string and they seem fine ####
puts "headers: "+headers.to_json
end
end
I have included the two helpers in rails_helper.rb as shown below:
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
RSpec.configure do |config|
config.include SessionHelper, type: :request
config.include RequestsHelper, type: :request
end
Finally I have a request spec in spec/request/posts/show_spec.rb
require 'rspec/its'
require 'spec_helper'
require 'rails_helper'
RSpec.describe 'GET /posts/:id', :type => :request do
let(:post) {create(:post)}
let(:id) {post.id}
let(:request_url) {"/posts/#{id}"}
context 'with a registered user' do
it 'has a status code of 200' do
get_with_token request_url
expect(response).to have_http_status(:success)
end
end
end
I expect this to pass but it fails with message:
Failure/Error: expect(response).to have_http_status(:success)
expected the response to have a success status code (2xx) but it was 403
The application works as expected on a browser.
What I'm a doing wrong?
gemfile:
source 'https://rubygems.org'
gem 'rails', '4.1.1'
gem 'mysql2
spec/spec_helper.rb
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment",__FILE__)
require 'rspec/rails'
require "capybara/rspec"
include Capybara::DSL
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
config.extend ControllerMacros, :type => :controller
end
spec/support/controller_macros.rb
module ControllerMacros
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryGirl.create(:user)
# user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the "confirmable" module
sign_in user
end
end
def load_key
session[:master_key] = SecureRandom.hex(24)
end
end
spec/factories/factory.rb
FactoryGirl.define do
factory :user do
email "test#test.com"
password "12345678"
end
end
categories_controller.rb
class CategoriesController < ApplicationController
before_filter :authenticate_user!, :load_key!
def index
#categories = Category.where("user_id is null or user_id = ?", current_user).order(updated_at: :desc)
end
private
def category_params
params.require(:category).permit(:title)
end
end
spec/controllers/categories_controller_spec.rb
require 'rails_helper'
describe CategoriesController do
login_user
load_key
it "get list of categories" do
get :index
expect(response).to render_template("index")
end
end
application_controller.rb
class ApplicationController < ActionController::Base
def load_key!
redirect_to(key_path) unless session[:master_key]
end
end
I would like to bypass a load_key! before_filer in CategoriesController. I added method load_key to controller_macros.rb. But it gives me the error:
undefined local variable or method 'session' for RSpec::ExampleGroups::CategoriesController:Class (NameError) from d:/sites/key_manager/spec/support/controller_macros.rb:19:in `load_key'
Looks like session variable is unavailable under rspec.
The answer was a quite simple.
I should use request.session rather than session.
In this case I should transfer a session variable assign from controller_macros.rb to categories_controller_spec.rb:
categories_controller_spec.rb
require 'rails_helper'
require 'spec_helper'
describe CategoriesController, :type => :controller do
login_user
it "redirect to key form when key was't loaded" do
get :index
expect(response).to redirect_to(key_path)
end
it "view password list #index" do
request.session[:master_key] = SecureRandom.hex(24) # <======================
get :index
expect(response).to render_template(:index)
end
end
controller_macros.rb
module ControllerMacros
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
sign_in FactoryGirl.create(:admin) # Using factory girl as an example
end
end
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryGirl.create(:user)
# user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the "confirmable" module
sign_in user
end
end
end