I am trying to get devise and devise-jwt gems to work so I can implement Authorization into my API only Rails app.
I have installed both devise and devise-jwt gems.
I followed the instructions on this blog post:
https://medium.com/#mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03
I have implemented the request specs that the author has included in his post and I can't get it approved on "Deleted",
I have to pass the authorizate token on delete, but I'm not getting it.
Any suggestion?
require 'rails_helper'
require "json"
RSpec.describe "POST /users", type: :request do
let(:user) { create(:user) }
let(:url) { '/users/sign_in' }
let(:params) do
{
user: {
email: user.email,
password: user.password
}
}
end
context 'when params are correct' do
before do
post url, params: params.to_json, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
it 'returns 200' do
expect(response).to have_http_status(200)
end
it 'returns JTW token in authorization header' do
expect(response.headers['authorization']).to be_present
end
end
context 'when login params are incorrect' do
before { post url }
it 'returns unathorized status' do
expect(response.status).to eq 401
end
end
end
RSpec.describe 'DELETE /logout', type: :request do
let(:url) { '/users/sign_out' }
it 'returns 204, no content' do
delete url
expect(response).to have_http_status(201)
end
end
I need to pass the user's token on delete, any suggestions on how I can be doing this?
I can't understand why my second test does not work.
This works perfectly fine:
require 'rails_helper'
require 'spec_helper'
RSpec.configure do |config|
config.include Devise::Test::IntegrationHelpers
end
RSpec.describe 'GET /users/:id', type: :request do
before(:all) do
#user = User.find_by(email: "user#dev.test")
sign_in #user
end
it "returns a user object" do
get "/users/#{#user.id}.json"
expect(response.status).to eq 200
expect(response).to have_http_status(:success)
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(JSON.parse(response.body)["successful"]).to eql(true)
end
end
but, if I add a second request in the same test like this:
require 'rails_helper'
require 'spec_helper'
RSpec.configure do |config|
config.include Devise::Test::IntegrationHelpers
end
RSpec.describe 'GET /users/:id', type: :request do
before(:all) do
#user = User.find_by(email: "user#dev.test")
sign_in #user
end
it "returns a user object" do
get "/users/#{#user.id}.json"
expect(response.status).to eq 200
expect(response).to have_http_status(:success)
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(JSON.parse(response.body)["successful"]).to eql(true)
end
it "returns another user object" do
get "/users/#{#user.id}.json"
expect(response.status).to eq 200
expect(response).to have_http_status(:success)
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(JSON.parse(response.body)["successful"]).to eql(true)
end
end
the test fails with error:
ActionController::RoutingError: No route matches [GET] "/users/2.json"
as you can see both tests are the same, but for some reason the second test always fail.
I think it is because there is no second User in Test Environment.
Add second user like this
before(:all) do
#user_second = User.create(user_params)
...
end
But best practice is to use gems like factory-bot and 'database-cleaner'. Also try to use 'byebug'.
I am trying to get devise and devise-jwt gems to work so I can implement Authorization into my API only Rails app.
I have installed both devise and devise-jwt gems.
I followed the instructions on this blog post:
https://medium.com/#mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03
I implemented the requests specs the author included in his post, and I can't get them to pass. If I put a byebug into the session controller, I see that it's saying the "User needs to sign in or sign up before continuing."
Any thoughts on what I'm doing incorrectly?
Here are the relevant files:
routes.rb
Rails.application.routes.draw do
namespace :api, path: '', defaults: {format: :json} do
namespace :v1 do
devise_for :users,
path: '',
path_names: {
sign_in: 'signin',
sign_out: 'signout',
registration: 'signup'
}
...
end
end
controllers/api/v1/sessions_controller.rb
class API::V1::SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(resource, _opts = {})
render json: resource
end
def respond_to_on_destroy
head :no_content
end
end
models/user.rb
class User < ApplicationRecord
devise :confirmable, :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :jwt_authenticatable, jwt_revocation_strategy: JwtBlacklist
...
end
models/jwt_blacklist.rb
class JwtBlacklist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Blacklist
self.table_name = 'jwt_blacklist'
end
config/initializers/devise.rb
Devise.setup do |config|
# Setup for devise JWT token authentication
config.jwt do |jwt|
jwt.secret = Rails.application.secret_key_base
jwt.dispatch_requests = [
['POST', %r{^*/signin$}]
]
jwt.revocation_requests = [
['DELETE', %r{^*/signout$}]
]
jwt.expiration_time = 1.day.to_i
end
config.navigational_formats = []
...
end
spec/request/authentication_spec.rb
require 'rails_helper'
describe 'POST /v1/signin', type: :request do
let(:user) { create(:user) }
let(:url) { '/v1/signin' }
let(:params) do
{
user: {
email: user.email,
password: user.password
}
}
end
context 'when params are correct' do
before do
post url, params: params
end
it 'returns 200' do
expect(response).to have_http_status(200)
end
it 'returns JTW token in authorization header' do
expect(response.headers['Authorization']).to be_present
end
it 'returns valid JWT token' do
decoded_token = decoded_jwt_token_from_response(response)
expect(decoded_token.first['sub']).to be_present
end
end
context 'when login params are incorrect' do
before { post url }
it 'returns unathorized status' do
expect(response.status).to eq 401
end
end
end
describe 'DELETE /v1/signout', type: :request do
let(:url) { '/v1/signout' }
it 'returns 204, no content' do
delete url
expect(response).to have_http_status(204)
end
end
I would expect the tests to pass, but I get the following errors:
Test Failures
Failures:
1) POST /v1/signin when params are correct returns 200
Failure/Error: expect(response).to have_http_status(200)
expected the response to have status code 200 but it was 401
# ./spec/request/authentication_spec.rb:21:in `block (3 levels) in <top (required)>'
2) POST /v1/signin when params are correct returns JTW token in authorization header
Failure/Error: expect(response.headers['Authorization']).to be_present
expected `nil.present?` to return true, got false
# ./spec/request/authentication_spec.rb:25:in `block (3 levels) in <top (required)>'
3) POST /v1/signin when params are correct returns valid JWT token
Failure/Error: decoded_token = decoded_jwt_token_from_response(response)
NoMethodError:
undefined method `decoded_jwt_token_from_response' for #<RSpec::ExampleGroups::POSTV1Signin::WhenParamsAreCorrect:0x00007fec3d3ae158>
# ./spec/request/authentication_spec.rb:29:in `block (3 levels) in <top (required)>'
Finished in 0.76386 seconds (files took 3.31 seconds to load)
5 examples, 3 failures
Failed examples:
rspec ./spec/request/authentication_spec.rb:20 # POST /v1/signin when params are correct returns 200
rspec ./spec/request/authentication_spec.rb:24 # POST /v1/signin when params are correct returns JTW token in authorization header
rspec ./spec/request/authentication_spec.rb:28 # POST /v1/signin when params are correct returns valid JWT token
I don't know if you found a solution; but I leave an approach I've made; It might helpfull.
Taking special attetion to the problem, The solution was to change:
decoded_token = decoded_jwt_token_from_response(response)
To:
decoded_token = JWT.decode(response.headers['authorization'].split(' ').last, Rails.application.credentials.jwt_secret, true)
Beacuse I din't find any in the documentation or other place and I chose to decode with method provided by JWT.
Also if you see I handle the requests in a different way, but I think that is not a problem at all.
require 'rails_helper'
require "json"
RSpec.describe "POST /login", type: :request do
let(:user) { User.create!( username: 'usertest',
email: 'usertest#email.com',
password: 'passwordtest123',
password_confirmation: 'passwordtest123') }
let(:url) { '/users/login' }
let(:params) do
{
user: {
login: user.email,
password: user.password
}
}
end
context 'when params are correct' do
before do
post url, params: params.to_json, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
it 'returns 200' do
expect(response).to have_http_status(200)
end
it 'returns JTW token in authorization header' do
expect(response.headers['authorization']).to be_present
end
it 'returns valid JWT token' do
token_from_request = response.headers['Authorization'].split(' ').last
decoded_token = JWT.decode(token_from_request, Rails.application.credentials.jwt_secret, true)
expect(decoded_token.first['sub']).to be_present
end
end
context 'when login params are incorrect' do
before { post url }
it 'returns unathorized status' do
expect(response.status).to eq 401
end
end
end
RSpec.describe 'DELETE /logout', type: :request do
let(:url) { '/users/logout' }
it 'returns 204, no content' do
delete url
expect(response).to have_http_status(204)
end
end
RSpec.describe 'POST /signup', type: :request do
let(:url) { '/users/signup' }
let(:params) do
{
user: {
username: 'usertest2',
email: 'usertest2#email.com',
password: 'passwordtest123',
password_confirmation: 'passwordtest123'
}
}
end
context 'when user is unauthenticated' do
before {
post url,
params: params.to_json,
headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
}
it 'returns 201' do
expect(response.status).to eq 201
end
it 'returns a new user' do
expect(response).to have_http_status :created
end
end
context 'when user already exists' do
before do
post url,
params: params.to_json,
headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
post url,
params: params.to_json,
headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
it 'returns bad request status' do
expect(response.status).to eq 400
end
it 'returns validation errors' do
expect(response_body['errors'].first['title']).to eq('Bad Request')
end
end
end
PD: I leave the spec code for register, with a couple differences (requests, url, username params in User model (that's is why I use the login param y the login request), I made all in a sigle spec.rb file, ...) to https://medium.com/#mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03. Kepp that in mind.
I believe you need to use the helper sign_in user before making the request for it to be authorized. Check https://github.com/heartcombo/devise, Controller tests
In controller current_user exists and is signed in, but request is unauthorized. Why is that?
module ControllerMacros
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryBot.create(:user, company: FactoryBot.create(:company))
sign_in user
end
end
end
rspec_helper.rb:
require 'spec_helper'
require 'devise'
require 'support/controller_macros'
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
config.include Devise::Test::ControllerHelpers, type: :controller
config.extend ControllerMacros, type: :controller
end
Users test:
require 'rails_helper'
RSpec.describe Api::V1::UsersController do
include Devise::Test::ControllerHelpers
describe "GET /api/v1/users" do
login_user
it "should get list of users" do
get 'index'
expect(response).to have_http_status(:success)
end
end
end
Error:
Failure/Error: expect(response).to have_http_status(:success)
This tests are passing and working fine:
it "should have a current_user" do
expect(subject.current_user).to_not eq(nil)
end
it "should be signed in" do
expect(subject.user_signed_in?).to be true
end
Try the below code:
require 'rails_helper'
RSpec.describe Api::V1::UsersController do
include Devise::Test::ControllerHelpers
describe "GET /api/v1/users" do
it "should get list of users" do
login_user
get 'index'
expect(response).to have_http_status(:success)
end
end
end
describe "action name or route " do
...omitted for brevity
it "should get list of users" do
get 'index', params: {}, headers: {...some_type_of_headers e.g BASIC, JWT}
expect(response).to have_http_status(:success)
end
end
I have a spec type: :request and I want to add authentication via ApiAuth. How can i do that?
ApiAuth use request object for authentication
ApiAuth.sign!(request, user.id, user.api_secret_key)
And I can use it in specs for controllers as follows
request.env['HTTP_ACCEPT'] = 'application/json'
user = defined?(current_user) ? current_user : Factory.create(:admin_user)
ApiAuth.sign!(request, user.id, user.api_secret_key)
But request object is missing in specs for requests
describe 'Cities API', type: :request do
let(:cities) { City.all }
let(:admin_user) { Factory.create(:admin_user) }
context 'given an unauthorized request' do
it 'returns 401 status' do
get '/api/cities'
expect(response).to have_http_status(:unauthorized)
end
end
context 'given an authorized request' do
it 'sends a list of cities' do
# i need authorization here
get '/api/cities'
expect(response).to be_success
end
end
end
Now I stub authentication
describe 'Cities API', type: :request do
let(:cities) { City.all }
let(:admin_user) { Factory.create(:admin_user) }
before { Factory.create(:route) }
context 'given an unauthorized request' do
it 'returns 401 status' do
get '/api/cities'
expect(response).to have_http_status(:unauthorized)
end
end
context 'given an authorized request' do
before(:each) do
allow_any_instance_of(CitiesController).to receive(:authenticate_user!).and_return(true)
allow_any_instance_of(CitiesController).to receive(:admin_user).and_return(admin_user)
end
it 'sends a list of cities' do
get '/api/cities'
expect(response).to be_success
end
end
end
But I want to avoid of using stubs in request specs.
Does anybody have any ideas?
I created issue on github. You can watch my solution here