I'm testing a RESTful api in rails using rspec.
My request looks like this:
before(:each) do
#user = FactoryGirl.create(:user)
sign_in(#user)
end
it "returns a 200 code when a user checks a valid token" do
get "/api/v1/users/#{#user.id}/token_check", token: #user.authentication_token
expect(response.code).to eql(200)
end
when i run the testing suite, I receive the error:
Failure/Error: get "/api/v1/users/#{#user.id}/token_check", token: #user.authentication_token
ActionController::UrlGenerationError:
No route matches {:action=>"/api/v1/users/1/token_check", :controller=>"api/v1/users", :token=>"6fgswkHwWXrcyDQNJVBZ"}
# ./spec/controllers/api/v1/users_controller_spec.rb:32:in `block (2 levels) in <top (required)>'
However, I can see the route for this action in rake routes:
api_v1_user_token_check GET /api/v1/users/:user_id/token_check(.:format) api/v1/users#token_check {:format=>:son}
I match this to my users_controller#token_check. Here is my controller and action:
def token_check
render json: {
result: ['Your authentication token is valid'],
}, status: 200
end
No route matches {:action=>"/api/v1/users/1/token_check", :controller=>"api/v1/users", :token=>"6fgswkHwWXrcyDQNJVBZ"}
As the error message shows, you just need to specify the action name.
from get "/api/v1/users/#{#user.id}/token_check", token: #user.authentication_token
to get "token_check", user_id: #user.id, token: #user.authentication_token
I was getting this same error. I solved this issue by simply moving it statement inside :type => :request describe statement like below:
describe 'GET /v1/feeds/' , :type => :request do
get '/api/v1/cars/random', xhr: true, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
expect(response.status).to eq 401
end
end
Related
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
I have the following situation:
EDITED
In my routes.rb
namespace :api, defaults: { format: :json } do
namespace :v1 do
# the definitions of other routes of my api
# ...
match '*path', to: 'unmatch_route#not_found', via: :all
end
end
EDITED
My controller:
class Api::V1::UnmatchRouteController < Api::V1::ApiController
def not_found
respond_to do |format|
format.json { render json: { error: 'not_found' }, status: 404 }
end
end
end
My test is as shown:
require 'rails_helper'
RSpec.describe Api::V1::UnmatchRouteController, type: :controller do
describe 'get response from unmatched route' do
before do
get :not_found, format: :json
end
it 'responds with 404 status' do
expect(response.status).to eq(404)
end
it 'check the json response' do
expect(response.body).to eq('{"error": "not_found"}')
end
end
end
It seems right to me, however I got the same error for both it statments:
1) Api::V1::UnmatchRouteController get response from unmatched route responds with 404 status
Failure/Error: get :not_found, format: :json
ActionController::UrlGenerationError:
No route matches {:action=>"not_found", :controller=>"api/v1/unmatch_route", :format=>:json}
# /home/hohenheim/.rvm/gems/ruby-2.3.1#dpms-kaefer/gems/gon-6.1.0/lib/gon/spec_helpers.rb:15:in `process'
# ./spec/controllers/api/v1/unmatch_route_controller_spec.rb:14:in `block (3 levels) in <top (required)>'
EDITED
The purpose with this route is be trigged when there's no other route possible in my api, with a custom json 404 response. This route and controller is working as expected right now, when we access routes like: /api/v1/foo or /api/v1/bar
How can I write the tests properly?
Additional info: Rails 4.2.6, Rspec 3.5.4
If you try to write routes spec, it won't work too and it will return something strange.
Failure/Error:
expect(get("/unmatch")).
to route_to("unmatch_route#not_found")
The recognized options <{"controller"=>"unmatch_route", "action"=>"not_found", "path"=>"unmatch"}> did not match <{"controller"=>"unmatch_route", "action"=>"not_found"}>, difference:.
--- expected
+++ actual
## -1 +1 ##
-{"controller"=>"unmatch_route", "action"=>"not_found"}
+{"controller"=>"unmatch_route", "action"=>"not_found", "path"=>"unmatch"}
Beside action not_found, it returned path => unmatch that maybe why controller spec didn't work as expected. Thus instead of controller test you can use request test as below.
require 'rails_helper'
RSpec.describe "get response from unmatched route", :type => :request do
before do
get '/not_found', format: :json
end
it 'responds with 404 status' do
expect(response.status).to eq(404)
end
it 'check the json response' do
expect(response.body).to eq('{"error": "not_found"}')
end
end
Take a look at this link:
https://apidock.com/rails/ActionDispatch/Routing/Mapper/Base/match
It says:
Note that :controller, :action and :id are interpreted as url query parameters and thus available through params in an action.
match ":controller/:action/:id"
Your route is:
match '*path', to: 'unmatch_route#not_found', via: :all
So your test is trying to find a route with :action=>"not_found" inside :controller=>"api/v1/unmatch_route". But your routes.rb does not have this route.
try something like this:
match 'unmatch_route/not_found', to: 'unmatch_route#not_found', via: :all
If you really need to use *path try this:
match '/:path/', :to => 'unmatch_route#not_found', :path=> /.*/, :as =>'not_found'
I also found myself wanting to test the response for API errors was rendering JSON, rather than writing a spec which simply rescued ActionController::RoutingError.
The following request spec worked for me, using Rails 6.0 & RSpec 3.9:
require 'rails_helper'
RSpec.describe '404 response for API endpoints' do
it 'renders an error in JSON' do
render_exceptions do
get '/api/v1/fictional-endpoint', headers: { 'Accept' => 'application/json' }
end
expect(response).to have_http_status(:not_found)
expect(response['Content-Type']).to include('application/json')
expect(json_response.fetch(:errors)).to include('Not found')
end
private
def json_response
JSON.parse(response.body, symbolize_names: true)
end
def render_exceptions
env_config = Rails.application.env_config
original_show_exceptions = env_config['action_dispatch.show_exceptions']
original_show_detailed_exceptions = env_config['action_dispatch.show_detailed_exceptions']
env_config['action_dispatch.show_exceptions'] = true
env_config['action_dispatch.show_detailed_exceptions'] = false
yield
ensure
env_config['action_dispatch.show_exceptions'] = original_show_exceptions
env_config['action_dispatch.show_detailed_exceptions'] = original_show_detailed_exceptions
end
end
References:
How to have Rails request specs handling errors like production
Comment regarding Rails.application.env_config caching
I am doing some simple integration testing. I want to test the sold_items action in the users controller. I have confirmed that the route exists and returns json by accessing it from the browser. However, rspec is telling me that the route doesn't exist. PLease see below, the spec, the error, and my route.rb. Thanks!
spec:
require 'rails_helper'
RSpec.describe UsersController, type: :controller do
describe "GET #items" do
it "returns http success" do
user = FactoryGirl.create(:user)
get "users/#{user.id}/sold_items"
expect(response).to have_http_status(:success)
end
end
end
fail message:
1) UsersController GET #items returns http success
Failure/Error: get "users/#{user.id}/sold_items"
ActionController::UrlGenerationError:
No route matches {:action=>"users/10/sold_items", :controller=>"users"}
# ./spec/controllers/users_controller_spec.rb:8:in `block (3 levels) in <top (required)>'
routes.rb
Rails.application.routes.draw do
resources :users
resources :items
get "users/:id/sold_items" => "users#sold_items"
EDIT
spec
require 'rails_helper'
RSpec.describe UsersController, type: :controller do
describe "GET #items" do
it "returns http success" do
user = FactoryGirl.create(:user)
get "users/:id/sold_items", id: user.id
expect(response).to have_http_status(:success)
end
end
end
failure message
2) UsersController GET #items returns http success
Failure/Error: get "users/:id/sold_items", id: user.id
ActionController::UrlGenerationError:
No route matches {:action=>"users/:id/sold_items", :controller=>"users", :id=>"12"}
# ./spec/controllers/users_controller_spec.rb:8:in `block (3 levels) in <top (required)>'
EDIT.2 MarvC second suggestion
require 'rails_helper'
RSpec.describe UsersController, type: [:request, :controller] do
describe "GET #items" do
it "returns http success" do
user = FactoryGirl.create(:user)
get "/users/:id/sold_items", id: user.id
expect(response).to have_http_status(:success)
end
end
end
failure
looks like the user.id isn't being passed in properly here
2) UsersController GET #items returns http success
Failure/Error: #sold_items = User.find(params[:id]).seller_items.sold
ActiveRecord::RecordNotFound:
Couldn't find User with 'id'=:id
# ./app/controllers/users_controller.rb:6:in `sold_items'
# ./spec/controllers/users_controller_spec.rb:8:in `block (3 levels) in <top (required)>'
One Solution:
when I use string interpolation to pass user.id into the url it works...
require 'rails_helper'
RSpec.describe UsersController, type: [:request, :controller] do
describe "GET #items" do
it "returns http success" do
user = FactoryGirl.create(:user)
get "users/#{user.id}/sold_items", id: user.id
expect(response).to have_http_status(:success)
end
end
end
It seems your syntax is wrong.
change:
get "users/#{user.id}/sold_items"
To:
rails 4
get "users/:id/sold_items", id: user.id
rails 5
get "users/:id/sold_items", params: { id: user.id }
I want to test my API with RSpec.
Api::V1::EventsController exists and has a create method. I use simple_token_authentication and pundit for security.
routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
# users/
resources :users, only: [:show, :update] do
# users/:id/events/
resources :events
end
end
end
end
Spec :
RSpec.describe Api::V1::EventsController, type: :controller do
describe 'events#create' do
before {
#user = User.create(email: 'm#m.fr', password: '12345678', password_confirmation: '12345678')
#user.reload
}
it 'should 401 if bad credentials' do
# Given the user
# When
post "/api/v1/users/#{#user.id}/events", {},
{
'x-user-email' => 'toto',
'x-user-token' => 'toto'
}
# Then
expect_status 401
end
end
end
And i get this error :
Failure/Error: post "/api/v1/users/#{#user.id}/events", {},
ActionController::UrlGenerationError:
No route matches {:action=>"/api/v1/users/1/events", :controller=>"api/v1/events"}
EDIT and answer :
I was confused and i was using rspec controller when i wanted to use rspec request.
Here's my working example :
RSpec.describe Api::V1::EventsController, type: :controller do
describe 'events#create' do
before {
#user = User.create(email: 'm#m.fr', password: '12345678', password_confirmation: '12345678')
#user.reload
}
it 'should 401 if bad credentials' do
# Given the user
# When
post "/api/v1/users/#{#user.id}/events", {}.to_json,
{
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'x-user-email' => 'toto',
'x-user-token' => 'toto'
}
# Then
expect_status 401
end
end
end
The post method in a controller spec takes an action name for the first parameter, not a path, so instead of:
post "/api/v1/users/#{#user.id}/events", #...
try:
post :create, #...
Controller specs are unit tests. If you want to test the entire stack, use a feature spec instead of a controller spec.
You don't need to do the request with the full path, because you already testing the Api::V1::EventsController.
So it will be much better to use special syntax for it:
post :create, nil, {
'x-user-email' => 'toto',
'x-user-token' => 'toto'
}
expect(response.response_code).to eq 401
If you want to test the route, you should do it in the routes specs:
# spec/routing/api_v1_events_routing_spec.rb
require "spec_helper"
RSpec.describe Api::V1::EventsController do
describe "routing" do
it "#create" do
expect(post: "/api/v1/users/1/events").to \
route_to(controller: "api/v1/events", action: "create", user_id: "1")
end
end
end
There is a method "sign_up" in controller
# controller/v3/users_controller
# POST api/v3/users/sign_up
def sign_up
user = User.new(params[:user])
if user.save && user.update_attribute(:channel, "user_#{user.id}")
render json: { Auth: { message: t(:sign_up_ok), user_id: user.id, channel: user.channel } }, status: 201
else
render json: { errors: Oj.load(user.errors.to_json) }, status: 400
end
end
route.rb
api_version(module: 'V3', path: 'api/v3') do
resources :users, only: [:index, :show, :destroy] do
collection do
post 'sign_up'
post 'sign_in'
end
end
I am trying to test it:
require 'spec_helper'
describe V3::UsersController do
describe "POST 'sign_up'" do
it "should be successful" do
post '/api/v3/users/sign_up'
assert_response 200
end
end
end
Get an error:
V3::UsersController POST 'sign_up' should be successful
Failure/Error: post '/api/v3/users/sign_up'
ActionController::RoutingError:
No route matches {:controller=>"v3/users", :action=>"/api/v3/users/sign_up"}
# ./spec/controllers/v3/user_controller_spec.rb:7:in `block (3 levels) in <top (required)>'
rake routes
sign_up_api_v3_users POST /api/v3/users/sign_up(.:format) V3/users#sign_up
Please, give me advice how to test this method?
ruby-1.9.3
rails-3.2.12
The action is not supposed to include the full path, just the name of the action.
post :sign_up
RSpec infers the rest of the path based on the controller your using. As you can see in the error message, it figured out the controller was v3/users, and it took your action, which is not a valid action.