0

my app is setting a current_user for all application, so that was ok until creating the test, in the main controller i am setting the user

class ApplicationController < ActionController::API
  ...

  def authenticate_action

  end

now i was writing the tests:

RSpec.describe 'Api::V1::Articles', type: :request do
  let(:user) {  FactoryBot.build_stubbed :user }
  
    describe 'POST /create' do
      context "with valid user params" do
        let!(:articles_params) { {article:{ name: "art1" } }}
        it 'creates a new article' do
          expect { post "/api/v1/posts/1/articles", params: articles_params }  .to change(Article, :count).by(1)
        end
      end
    end
end

but that user in the test is only fake, thats why i am getting this error:

Completed 400 Unauthorized
register
  • 9
  • 3
  • What does the `bearer_token` method look like? – spickermann Nov 11 '22 at 05:12
  • @spickermann from ui (firebase) we are sending the token bearer – register Nov 11 '22 at 05:58
  • @spickermann by any chance other question please, how can i improve this part "/api/v1/posts/1/articles", i would like just use post :create but it does not worked – register Nov 11 '22 at 06:03
  • @spickermann help me please with this question more please https://stackoverflow.com/questions/74398500/how-to-create-update-test-with-rspec – register Nov 11 '22 at 06:04
  • You cannot use `post :create` in request test, that only works in controller tests. Request test need the whole path because they test routing too. See this question for details: https://stackoverflow.com/questions/40851705/controller-specs-vs-request-specs – spickermann Nov 11 '22 at 08:45
  • @spickermann i want to test the controller not request, but i read that test for controllers are obsolete – register Nov 11 '22 at 11:46

1 Answers1

1

It depends on if you want to actually test your authentication too on every request. If that is the case, you need to set a valid auth headers for that user, like this:

RSpec.describe 'Api::V1::Articles', type: :request do
  let(:user) {  FactoryBot.create :user }
  let(:auth_header) { { 'Authorization' => "TOKEN#{user.generate_bearer_token}" } }

  describe 'POST /create' do
    context "with valid user params" do
      let(:articles_params) { {article:{ name: "art1" } }}

      it 'creates a new article' do
        expect { 
          post "/api/v1/posts/1/articles", params: articles_params, headers: auth_header
        }.to change(Article, :count).by(1)
      end
    end
  end
end

Note that you need to change "TOKEN#{user.generate_bearer_token}" in the third line of the above example with an implementation from your application to generate a valid bearer token for the given user.

Or you can decide to not care about how authentication is implemented, and mock the whole authentication logic in the test. Then you have, of course, to test the authentication logic in other places to ensure the implementation actually works in general.

To mock the implementation, I would first move parts of the authenticate_action method into a class method in the user model or a class in the Auth namespace and then just call that one method in authenticate_action:

# in app/models/user.rb
def self.find_by_bearer_token(token)
  id = Auth::TokenValidator.call(token).result
  User.find(id) if id
end

# in your application_controller
def authenticate_action
  @user = User.find_by_bearer_token(bearer_token)

  render json: 400, status: :unauthorized unless @user
end

With such a class method, it is much easier to mock that method in the test and return whatever user you want to use for the spec:

RSpec.describe 'Api::V1::Articles', type: :request do
  let(:user) {  FactoryBot.create :user }
  
  before { allow(User).to receive(:find_by_bearer_token).and_return(user) }

  describe 'POST /create' do
    context "with valid user params" do
      let(:articles_params) { {article:{ name: "art1" } }}

      it 'creates a new article' do
        expect { 
          post "/api/v1/posts/1/articles", params: articles_params
        }.to change(Article, :count).by(1)
      end
    end
  end
end
spickermann
  • 100,941
  • 9
  • 101
  • 131
  • but this "TOKEN#{user.generate_bearer_token}" just it will work some minutes – register Nov 11 '22 at 21:06
  • in this case find_by_bearer_token(bearer_token) also i will need a bearer token? i ask you that because the token only will like some minutes – register Nov 11 '22 at 21:45
  • Yes, in that case you would need a bearer token. Ideally, you generate a new bearer token for each test automatically. This ensures that the token and authentication really works as expected for each method. The other version with a mocked authentication is certainly faster but you will not notice when auth is broken for a method. – spickermann Nov 12 '22 at 06:28
  • thanks, but is there another way to bypass the authentication? or mock the authentication without bearer_token – register Nov 12 '22 at 14:00
  • The second version (with the mock) doesn't require a valid bearer token. Only the first version does. – spickermann Nov 12 '22 at 17:05
  • i am getting this error Failure/Error: @user = find_by_bearer_token(bearer_token) NoMethodError: undefined method `find_by_bearer_token' – register Nov 14 '22 at 11:51
  • I updated my answer to address this issue. Just change that line to `@user = User.find_by_bearer_token(bearer_token)` – spickermann Nov 14 '22 at 13:46