Why does Rails RSpec response show 302 instead of 401?
Asked Answered
N

3

11

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?

Nanny answered 31/3, 2016 at 18:13 Comment(4)
Perhaps add puts response.body and puts response.current_path to find out where it's redirecting to?Franzoni
This is a shot in the dark, but try PATCH instead of PUT ? I vaguely recall that PATCH is now rails standard and it might be redirecting a PUT to PATCH ???Borax
Check if redirect from ApplicationController?Pelag
I think the problem is definitely in before_action. You should handle it.You can try debug your tests installing gem 'byebug' and adding code require 'byebyg'; byebug .Psychotomimetic
M
3

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 the case that worked for me, yours may be different.

Mehta answered 24/7, 2019 at 21:7 Comment(0)
T
1

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' }
Tyner answered 31/8, 2021 at 13:45 Comment(0)
B
1

I believe the issue is that devise returns HTTP 302 Redirect if the request is made from a browser, otherwise it returns 401 Unauthorized.

I believe the solution is to update the requests in your test file to look like this:

put :update, topic_id: my_topic.id, id: my_post.id, post: {title: my_post.title, body: my_post.body}, as: :json
Beauvoir answered 24/6, 2022 at 11:24 Comment(1)
yes, the only thing was missing is as: :jsonOlivann

© 2022 - 2024 — McMap. All rights reserved.