I have been stuck on this problem for several days and I don't know what is wrong with it. I started Ruby on Rails few months ago and I am currently learning authentication with API. I have looked at other similar topics here and there but none of them helped.
My problem is whenever I run RSpec on this code (located at spec/api/v1/controllers/posts_controller_spec.rb
)
require 'rails_helper'
RSpec.describe Api::V1::PostsController, type: :controller do
let(:my_user) { create(:user) }
let(:my_topic) { create(:topic) }
let(:my_post) { create(:post, topic: my_topic, user: my_user) }
context "unauthenticated user" do
it "PUT update returns http unauthenticated" do
put :update, topic_id: my_topic.id, id: my_post.id, post: {title: my_post.title, body: my_post.body}
expect(response).to have_http_status(401)
end
...
I keep getting
...
spec/api/v1/controllers/posts_controller_spec.rb -e "unauthenticated user"
Run options: include {:full_description=>/unauthenticated\ user/}
FFF
Failures:
1) PostsController unauthenticated user PUT update returns http unauthenticated
Failure/Error: expect(response).to have_http_status(401)
expected the response to have status code 401 but it was 302
# ./spec/api/v1/controllers/posts_controller_spec.rb:12:in `block (3 levels) in <top (required)>'
If this helps, my controller, app/controllers/api/v1/posts_controller_spec.rb
has
class Api::V1::PostsController < Api::V1::BaseController
before_action :authenticate_user, except: [:index, :show]
before_action :authorize_user, except: [:index, :show]
def update
post = Post.find(params[:id])
if post.update_attributes(post_params)
render json: post.to_json, status: 200
else
render json: {error: "Post update failed", status: 400}, status: 400
end
end
...
My base_controller
class Api::V1::BaseController < ApplicationController
skip_before_action :verify_authenticity_token
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :malformed_request
def authenticate_user
authenticate_or_request_with_http_token do |token, options|
@current_user = User.find_by(auth_token: token)
end
end
def authorize_user
unless @current_user && @current_user.admin?
render json: {error: "Not Authorized", status: 403}, status: 403
end
end
def malformed_request
render json: {error: "The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications.", status: 400}, status: 400
end
def not_found
render json: {error: "Record not found", status: 404}, status: 404
end
end
Lastly, routes.rb shows:
namespace :api do
namespace :v1 do
resources :users, only: [:index, :show, :create, :update]
resources :topics, except: [:edit, :new] do
resources :posts, only: [:update, :create, :destroy]
end
end
end
I have a strong hunch that my before_action is the culprit, but I can't pinpoint what I did wrong. I have done similar thing on Topics (Posts are nested within Topics) using similar RSpec testing and my Topic RSpec passes, whereas my Posts RSpec fail miserably.
Can someone please help me to point out what I did wrong, and how can I pass the RSpec test and not show 302 status code?
As an extra, why did it show 302 status code?
I use the database_cleaner gem to scrub my test database before each test runs, ensuring a clean slate and stable baseline every time. By default, RSpec will actually do this for you, running every test with a database transaction and then rolling back that transaction after it finishes.
The word describe is an RSpec keyword. It is used to define an “Example Group”. You can think of an “Example Group” as a collection of tests. The describe keyword can take a class name and/or string argument.
RSpec is a testing framework written in Ruby to test Ruby code. To get started using RSpec with Rails, add it to the Gemfile. group :development, :test do gem 'rspec-rails' end. Next finish setting it up by running bundle install in your project directory and then. rails generate rspec:install.
RSpec is a testing tool for Ruby, created for behavior-driven development (BDD). It is the most frequently used testing library for Ruby in production applications. Even though it has a very rich and powerful DSL (domain-specific language), at its core it is a simple tool which you can start using rather quickly.
The problem is that you're using
before_action :authenticate_user, except: [:index, :show]
, probably the helper from Devise, correct? If so, when you try to access a resource without being logged in, this helper will redirect you to the login page, something that you expect to happen when you're using a browser, which is not your case. You can find some alternatives, overwriting the Devise behavior, my solution was to define my authenticate_user!
method to return a 401 if the user was not signed in, using another devise helper method, something like this:
def authenticate_user!
if user_signed_in?
@user = current_user
else
head :unauthorized
end
end
This is just the case that worked for me, yours may be different.
I ran into to same issue, while normally using
let(:auth_headers) { Devise::JWT::TestHelpers.auth_headers({ 'ACCEPT': 'application/json' }, user) }
# ...
get "/api/v1/something", headers: auth_headers
in my request specs.
I first tried to test the 401 Unauthorized case by just removing auth_headers
, which then lead to the unexpected 302 Found response.
However, testing my API via Postman worked as expected (401 response without auth header).
The solution was to just remove the Authorization
header and keeping the Accept
header, so the application would know that it's not talking to a user and thus would not redirect to the login page:
let(:auth_headers) { Devise::JWT::TestHelpers.auth_headers({ 'ACCEPT': 'application/json' }, user) }
# ...
get "/api/v1/something", headers: { 'ACCEPT': 'application/json' }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With