简体   繁体   中英

setting up minitest with Devise with refactoring test_helper

Rails 7 application using devise for authentication.

fixture (model tests pass for all classes):

one: 
  email: 'me@mail.co'
  encrypted_password: <%= User.new.send(:password_digest, '12345678')

test_helper which attempts to re-factor the login and action process into short one-liners ( test_access ) in the actual controller tests

require 'simplecov'
SimpleCov.start 'rails'

ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require 'webmock/minitest'
require 'barby/outputter/png_outputter'
require 'barby/barcode/ean_13'

class ActiveSupport::TestCase
  fixtures :all
  include Devise::Test::IntegrationHelpers
  include Warden::Test::Helpers

  parallelize(workers: :number_of_processors)

  def log_in(user)
    if integration_test?
      login_as(user, :scope => :user)
    else
      sign_in(user)
    end
  end

  class Minitest::Test

    def setup
      @one = users(:one)
    end
  end

  private
    def test_access(user, action)
      get '/users/sign_in'
puts user.inspect
      sign_in user
# puts user_signed_in?
# puts current_user.manager?
puts user.manager?
      post user_session_url
      follow_redirect!
puts action
      get action
      assert_response :success
    end

and the controller_test (presently on an intermediate refactoring level, as the method should extend to an array of users)

require "test_helper"

class LokationsControllerTest < ActionDispatch::IntegrationTest
  include Devise::Test::IntegrationHelpers


  setup do
    @users_allowed = [ @one ]
    @actions = [dummy_lokations_url]
  end

  test "should get dummy" do
    @actions.each do |action|
      test_access(@one, action)
      assert_response :success
    end
  end

the problem is there seems to be no way of verifying whether the user is signed in or not, notwithstanding the inclusion of devise integration helpers. The output in the console points to proper values, but the result is the opposite of the method that should be applied

#<User id: 760812109, email: [...]>
true
http://www.example.com/lokations/dummy
[...]
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/>
Response body: <html><body>You are being <a href="http://www.example.com/">redirected</a>.</body></html>

because the before_action method applied on the action specifies

  def index_manager
    if user_signed_in? && current_user.manager?
    else
      redirect_to root_path and return
    end
  end

Where is the above mistaken? (I have a nagging doubt that something may be missing as this pattern has been successfully followed in the past)

Sorry but this is just not a good/acceptable refactor.

  • Do not set the password digest - ever. That should only be known by the underlying system that encrypts the password. Your code and tests should not even know that it exists. Your code should only ever set the password attribute with a cleartext.

  • These are integration and not controller tests. Controller tests are subclasses of ActionController::TestCase used in legacy applications. They should not be used. While this might seem like nickpicking you're only confusing yourself and others by conflating them.

  • Avoid monkeypatching a bunch of junk into ActiveSupport::TestCase or even worse Minitest::Test . If you MUST monkeypatch then write your methods in a module and include it so that the stack trace points there. But do it at the correct level in the class heirarchy. For example separate between the helpers for integration and system tests and include them into ActionDispatch::IntegrationTest and ActionDispatch::SystemTest instead of polluting all your tests.

  • If you need to add a lot of additional behavior don't reopen the class. Instead create your own test class which inherits from ActionDispatch::IntegrationTest and have your tests subclass it.

  • If you have code that you want to reuse in multiple tests like the test setup use modules that can be included where they are actually needed.

  • test_access doesn't belong in the shared subclass way up the tree. This belongs in an actual integration test which covers your authentication system flows or that a specific resource is authorized correctly.

  • Devise is an authentication system (who is the user?) - but what you're doing here is authorization (who can do what?). Devise doesn't provide any kind of authorization besides preventing access for users that are not authenticated. Neither should it (see SRP ).

  • The index_manager method isn't a good way to implement authorization. If you want to reinvent the wheel make sure you raise an exception and use rescue_from so that the callback chain is halted and so that you're not repeating yourself. You may want to look into existing systems like CanCanCan and Pundit.

What you actually want here is something more like:

class AuthorizationError < StandardError
end
class ApplicationController
  before_action :authenticate_user! # use an opt-out secure by default setup.
  rescue_from AuthorizationError, with: :deny_access

  private

  def authorize_manager!
    unless current_user.manager?
      raise AuthorizationError.new("You need to be a manager to access this resource") 
    end
  end

  def deny_access(exception)
    redirect_to root_path, error: exception.message
  end
end
module AuthorizationIntegrationHelpers
  def assert_denies_access
    follow_redirect!
    assert_current_path root_path
    # ...
  end
end
class FoosFlowTest < ActionDispatch::IntegrationTest
  include AuthorizationIntegrationHelpers
  include Warden::Test::Helpers

  test "should not be accessable to non-managers" do
    login_as(users(:one))
    get '/foos'
    assert_denies_access
  end
end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM