【问题标题】:Trouble with Capybara and Admin User permissions (and Devise)Capybara 和管理员用户权限(和设计)的问题
【发布时间】:2026-01-15 22:15:01
【问题描述】:

设计用于用户注册和登录。

使用 Rails 控制台设置管理员(将 admin 布尔值设置为 true)。

Rspec 和 FactoryGirl。

不幸的是,我在编写测试之前编写了我的应用程序(最好的教训是通过艰难的方式学习的)。我现在正在学习 rspec 并为该应用编写测试套件。

我为管理员和非管理员设置了控制器权限和查看权限,这在实践中确实有效(我通过彻底的浏览器手动测试知道这一点)。

在这种情况下,我在标题中有一个“管理员”链接,当用户登录并且也是管理员时显示该链接。

我的 StaticPagesController 也有一个有效的 before_action 设置,所以没有人可以访问管理页面,除非他们已经登录并且也是管理员。

我为此编写了一些测试,并认为我已经对其进行了排序,直到我注意到,当对包含这些测试的特定 /features 规范文件进行更改时,guard 只运行这些测试并通过它们。但是,当我运行整个测试套件时,同样的测试会失败。我完全被这个弄糊涂了。

我认为这可能与 Devise 有关,但我只是不知道。

spec/features/user_and_role_spec.rb

require 'rails_helper'

def manually_create_user
    visit new_user_registration_path
    fill_in('user_first_name', :with => 'Test')
    fill_in('user_last_name', :with => 'User')
    fill_in('user_email', :with => 'testuser@email.com')
    fill_in('user_password', :with => 'testuser')
    click_button('Sign up')
end

def create_user_and_login_as(type)
    user = FactoryGirl.create(type)
    visit(new_user_session_path)
    fill_in('user_email', :with => user.email)
    fill_in('user_password', :with => user.password)
    click_button('Log in')
end


describe 'with users and roles' do  

    context "if user is not an admin" do

        it "does not allow any user to visit the admin page if not logged-in" do
            visit(admin_path)
            expect(current_path).to eq(root_path)
        end

        it "does not allow a new user to visit the admin page" do 
            manually_create_user
            visit(admin_path)
            expect(current_path).to eq(root_path)
        end

        it "does not allow a student to visit the admin page" do 
            create_user_and_login_as(:student)
            visit admin_path
            expect(current_path).to eq(root_path)
        end

        it "does not allow a teacher to visit the admin page" do 
            create_user_and_login_as(:teacher)
            visit admin_path
            expect(current_path).to eq(root_path)
        end

    end


    context "if user is an admin" do

        it "allows an admin user to visit the admin page" do 
            create_user_and_login_as(:admin_user)
            click_link 'Admin'
            expect(current_path).to eq(admin_path)
        end

        it "allows a teacher_admin to visit the admin page" do 
            create_user_and_login_as(:teacher_admin_user)
            click_link 'Admin'
            expect(current_path).to eq(admin_path)
        end

    end

end

运行完整测试套件时,上下文“如果用户不是管理员”中的测试全部失败。它们都因相同的错误而失败:

Failure/Error: expect(current_path).to eq(root_path)

       expected: "/"
            got: "/admin"

       (compared using ==)

对我来说,这意味着管理页面可以访问,而它本来不应该访问。在我的浏览器中,无法看到管理页面链接,也无法通过手动输入 url 访问该页面,除非用户已登录并且是管理员。

运行完整测试套件时,上下文“如果用户是管理员”中的测试全部通过。

spec/factories/users.rb:

require 'faker'

FactoryGirl.define do
    factory :user do |f|
        f.first_name    { Faker::Name.first_name }
        f.last_name     { Faker::Name.last_name }
        f.email         { Faker::Internet.email }
        f.password      { Faker::Internet.password(8) }
        f.admin         false

        trait :student do
            type "Student"
        end

        trait :teacher do 
            type "Teacher"
        end

        trait :admin do 
            admin true
        end

        factory :admin_user,            traits: [:admin]
        factory :student,               traits: [:student]
        factory :teacher,               traits: [:teacher]
        factory :teacher_admin_user,    traits: [:teacher, :admin]

    end
end

static_pages_controller.rb:

class StaticPagesController < ApplicationController
    before_action :admin?, only: [:admin]

  def home
    @testimonials = Testimonial.all
  end

  def admin
    @groups = Group.all

    @users = User.all

    @students = Student.all

    @teachers = Teacher.all
  end

  private

  def admin?
    unless signed_in? and current_user.admin == true
        redirect_to root_path, notice: "You must be a signed-in admin to view this page"
    end
  end

end

static_pages_helper.rb:

module StaticPagesHelper

    def allowed_to_see_admin_link?
        signed_in? && current_user.admin
    end

end

models/user.rb:

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  validates :first_name, presence: true
  validates :last_name, presence: true
  validates :admin, inclusion: { in: [true, false] }

  scope :newest_first, -> { order("created_at DESC") }
  scope :order_by_first_name, -> { order("first_name") }

  def full_name
    "#{first_name} #{last_name}"
  end

  def unassigned?
    type != "Student" and type != "Teacher"
  end

  def can_view_materials?
    admin || type == "Teacher" || type == "Student" && groups.any? # So only current students can view the Materials page.
  end

  def testimonial_owner?(testimonial)
    id == testimonial.student_id
  end

end

_header.html.erb 部分的相关部分:

<ul class="nav navbar-nav navbar-right">
        <% if allowed_to_see_admin_link? %>
          <li><%= link_to "Admin", admin_path %></li>
        <% end %>
      </ul>

宝石文件:

gem 'rails', '4.2.0'
gem 'bootstrap-sass', '~> 3.3.3'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails'
gem 'turbolinks'
gem 'jbuilder', '~> 2.0'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'devise'

group :development, :test do
    gem 'sqlite3'
    gem 'byebug'
    gem 'web-console', '~> 2.0'
    gem 'spring'
    gem 'better_errors'
    gem 'binding_of_caller'
    gem 'rspec-rails'
    gem 'guard-rspec', require: false
    gem 'factory_girl_rails'
end

group :test do
    gem 'faker'
    gem 'capybara'
    gem 'launchy'
    gem 'database_cleaner'
end

group :production do
    gem 'pg'
    gem 'rails_12factor'
end

架构中的用户:

create_table "users", force: :cascade do |t|
    t.string   "email",                  default: "",    null: false
    t.string   "encrypted_password",     default: "",    null: false
    t.string   "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.integer  "sign_in_count",          default: 0,     null: false
    t.datetime "current_sign_in_at"
    t.datetime "last_sign_in_at"
    t.string   "current_sign_in_ip"
    t.string   "last_sign_in_ip"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.boolean  "admin",                  default: false
    t.string   "type"
    t.string   "first_name"
    t.string   "last_name"
  end

routes.rb:

resources :materials

  root 'static_pages#home'

  devise_for :users, :controllers => { registrations: 'registrations' }

  get 'admin' => 'static_pages#admin'

  resources :groups
  resources :users
  resources :students
  resources :teachers
  resources :testimonials
  post 'assign_to_group' => 'students#assign_to_group' # Could have been 'patch', but default in the controller method is 'post', so I left the method as default and changed this route to 'post'. Doesn't NEED to be patch.
  post 'remove_from_group' => 'students#remove_from_group'
  post 'unassign_teacher' => 'groups#unassign_teacher'
  post 'assign_as_student' => 'teachers#assign_as_student'
  post 'assign_as_teacher' => 'students#assign_as_teacher'
  post 'add_student' => 'groups#add_student'
  post 'remove_student_from_group' => 'groups#remove_student_from_group'

【问题讨论】:

  • 在您的规范中 - 尝试检查用户在管理页面上实际看到/看不到的内容,例如 expect(page).not_to have_content("Welcome Mr Admin") 或登录管理页面上实际显示的内容 - 如果您可以看到,那么是的,您需要弄清楚为什么人们会自动登录到管理员。否则可能是他们在管理员登录页面上???
  • 另外,风格要点:使用&amp;&amp;|| 而不是andor(在这里阅读原因:devblog.avdi.org/2010/08/02/using-and-and-or-in-ruby)其次:current_user.admin == true 这非常危险。 . 如果您不小心忘记了其中一个= 怎么办 - 这是一个很难发现的错误,实际上不会导致您的代码失败……只是为了让所有用户静默地看到管理页面。 总是 这样做是一个好习惯:true == current_user.admin 如果你不小心改用=,那将会失败

标签: ruby-on-rails-4 rspec capybara factory-bot


【解决方案1】:

您的测试失败的原因是您在重定向完成之前检查 current_path。基本上你正在调用 visit(xxx),它将 current_path 设置为 xxx,然后立即读回 xxx,而服务器将重定向返回到 /,然后浏览器将 current_path 更改为 /。只要您使用 Capybara 2.5+,您就应该使用 have_current_path 匹配器,它会重试一下,从而给重定向时间进行处理

expect(page).to have_current_path(root_path)

【讨论】:

    最近更新 更多