Capybara/Rspec - 在 Ajax 请求的第二种情况下会话丢失

Capybara/Rspec - Session Lost on second scenario with Ajax request

我正在努力解决这个问题。我有一个正在编写的功能规范,其中我编写了多个场景,我希望它们在同一个用户会话中全部 运行。测试 运行 很好,直到它到达最后一个发出单独 AJAX 请求的场景,然后在该请求上产生 401 Unauthorized。我已经在互联网上研究了几个小时,但没有运气。希望有人能指出这里发生了什么。

我正在使用 Devise 进行身份验证,Selenium-webdriver & Rails 6.

这是我的相关文件:

rails_helper.rb:

# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require 'pry'
require 'spec_helper'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'shoulda/matchers'
require 'pundit/matchers'
require 'brakeman'

Delayed::Worker.delay_jobs = false

# Add additional requires below this line. Rails is not loaded until this point!

# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end
# in _spec.rb will both be required and run as specs, causing the specs to be
# run twice. It is recommended that you do not name files matching this glob to
# end with _spec.rb. You can configure this pattern with the --pattern
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
#
# The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

# RSpec Examples run within a transaction
# Preload our constants before any transaction begins
# Constants.constants

RSpec.configure do |config|

  config.include Devise::Test::ControllerHelpers, type: :controller

  config.expect_with :rspec do |expectations|
    expectations.syntax = [:should, :expect]
  end

  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  # config.fixture_path = "#{::Rails.root}/spec/fixtures"

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  # config.use_transactional_fixtures = false

  # RSpec Rails can automatically mix in different behaviours to your tests
  # based on their file location, for example enabling you to call `get` and
  # `post` in specs under `spec/controllers`.
  #
  # You can disable this behaviour by removing the line below, and instead
  # explicitly tag your specs with their type, e.g.:
  #
  #     RSpec.describe UsersController, :type => :controller do
  #       # ...
  #     end
  #
  # The different available types are documented in the features, such as in
  # https://relishapp.com/rspec/rspec-rails/docs
  config.infer_spec_type_from_file_location!
end

spec/support/capybara.rb:

# require 'capybara/rails'
require 'capybara/rspec'

Capybara.server = :puma
Capybara.server_host = 'intranet.lvh.me'
Capybara.default_driver = :selenium_chrome
Capybara.javascript_driver = :selenium_chrome
Capybara.default_max_wait_time = 10

def sign_in_helpdesk
  user = FactoryBot.create(:user, :helpdesk_technician)
  user = user.becomes(HolmanPermissions::User)
  user.user_grants << HolmanPermissions::UserGrant.new({
    permission: AuthDomain::Permission.find_by(name: 'IT Technician'),
    tenant: AuthDomain::Tenant.find_by(name: 'All Locations')
  })

  login_as(user.becomes(::User), scope: :user, run_callbacks: false)
  user
end

RSpec.configure do |config|
  config.after(:each, type: :feature) do
    Capybara.current_session.instance_variable_set(:@touched, false)
  end
  config.after(:all, type: :feature) do
    Capybara.current_session.instance_variable_set(:@touched, true)
  end
end

spec/support/database_cleaner.rb:

Constants.constants
RSpec.configure do |config|

  config.use_transactional_fixtures = false
  config.include Warden::Test::Helpers
  Warden.test_mode!

  config.before(:suite) do
    if config.use_transactional_fixtures?

      raise(<<-MSG)
        Delete line `config.use_transactional_fixtures = true` from rails_helper.rb
        (or set it to false) to prevent uncommitted transactions being used in
        JavaScript-dependent specs.
        During testing, the Ruby app server that the JavaScript browser driver
        connects to uses a different database connection to the database connection
        used by the spec.
        
        This Ruby app server database connection would not be able to see data that
        has been setup by the spec's database connection inside an uncommitted
        transaction.
        Disabling the use_transactional_fixtures setting helps avoid uncommitted
        transactions in JavaScript-dependent specs, meaning that the Ruby app server
        database connection can see any data set up by the specs.
      MSG

    end
  end

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
    ReferenceCreator.new
    Constants.reload
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, type: :feature) do
    # :rack_test driver's Rack app under test shares database connection
    # with the specs, so we can use transaction strategy for speed.
    driver_shares_db_connection_with_specs = Capybara.current_driver == :rack_test

    if driver_shares_db_connection_with_specs
      DatabaseCleaner.strategy = :transaction
    else
      # Non-:rack_test driver is probably a driver for a JavaScript browser
      # with a Rack app under test that does *not* share a database
      # connection with the specs, so we must use truncation strategy.
      DatabaseCleaner.strategy = :truncation
    end
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
    Warden.test_reset!
  end

end

config/initializers/session_store.rb:

Core::Application.config.session_store :cookie_store, key: '_Core_session', domain: :all

config/environments/test.rb:

Rails.application.configure do
  $VERBOSE = nil
  # Settings specified here will take precedence over those in config/application.rb.

  # RAILS 5.2 CONFIG
  # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
  # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
  config.require_master_key = true


  # RAILS 5.1 CONFIG
  # Attempt to read encrypted secrets from `config/secrets.yml.enc`.
  # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
  # `config/secrets.yml.key`.
  config.read_encrypted_secrets = true


  # The test environment is used exclusively to run your application's
  # test suite. You never need to work with it otherwise. Remember that
  # your test database is "scratch space" for the test suite and is wiped
  # and recreated between test runs. Don't rely on the data there!
  config.cache_classes = true

  # Do not eager load code on boot. This avoids loading your whole application
  # just for the purpose of running a single test. If you are using a tool that
  # preloads Rails for running tests, you may have to set it to true.
  config.eager_load = false

  # Configure public file server for tests with Cache-Control for performance.
  config.public_file_server.enabled = true
  config.public_file_server.headers = {
    'Cache-Control' => "public, max-age=#{1.hour.to_i}"
  }

  # Show full error reports and disable caching.
  config.consider_all_requests_local       = true
  config.action_controller.perform_caching = false
  config.cache_store = :null_store

  # Raise exceptions instead of rendering exception templates.
  config.action_dispatch.show_exceptions = true

  # Disable request forgery protection in test environment.
  config.action_controller.allow_forgery_protection = false

  # Store uploaded files on the local file system in a temporary directory
  # config.active_storage.service = :test

  config.action_mailer.perform_caching = false

  # Tell Action Mailer not to deliver emails to the real world.
  # The :test delivery method accumulates sent emails in the
  # ActionMailer::Base.deliveries array.
  config.action_mailer.delivery_method = :test

  config.action_mailer.default_url_options = { host: 'intranet.lvh.me:3000' }

  # Print deprecation notices to the stderr.
  config.active_support.deprecation = :log

  config.assets.debug = true

  # Raises error for missing translations
  # config.action_view.raise_on_missing_translations = true
end

spec/features/helpdesk_spec.rb:

require 'rails_helper'

RSpec.feature "Helpdesk Ticketing System", js: true, type: :feature do
  before(:all) do
    # Capybara.current_session.instance_variable_set(:@touched, false)
    @user = sign_in_helpdesk
    @ticket = FactoryBot.create(:ticket, ticket_type_id: Constants.helpdesk.ticket_types.it_id)
    @second_helpdesk_tech = FactoryBot.create(:user, first_name: 'Simon', last_name: 'Helpdesk', email: 'simon@helpdesk.com')
    @second_helpdesk_tech = @second_helpdesk_tech.becomes(HolmanPermissions::User)
      @second_helpdesk_tech.user_grants << HolmanPermissions::UserGrant.new({
      permission: AuthDomain::Permission.find_by(name: 'IT Technician'),
      tenant: AuthDomain::Tenant.find_by(name: 'All Locations')})
    visit tickets_path
  end
  
  context 'viewing helpdesk contents' do
    scenario 'should have visible helpdesk data' do
      expect(page).to have_content('Open Tickets')
      expect(page).to have_selector("div.ticket", count: 1)
      expect(page).to have_content(@ticket.summary)
    end
  end

  context 'assigning a ticket' do
    before do
      first('.popover_link').click
      popover = first('div.popover')
      expect(popover).to have_content('Assign')

      first('.link_to_assign').click
      sleep 2
    end
    after do
      page.evaluate_script('$("#assign-modal").modal("hide")')
    end
    scenario 'should show the assign modal' do
      expect(page).to have_selector('#assign-modal', visible: true)
    end
    scenario 'should assign the technician selected' do # This is the scenario that makes an ajax call
      assign_modal = find('#assign-modal')
      expect(assign_modal).to have_selector('#assign-technician')
      within '#assign-modal' do
        select(@second_helpdesk_tech.name, from: 'assign-technician')
        click_on 'Assign' # ajax call made upon click

        sleep 2
      end
      expect(first('div.ticket .status')).to have_text(@second_helpdesk_tech.name)
    end
  end
end

所有断言都通过,直到最后一个。这是打开控制台选项卡时的屏幕截图。

在遵循@ThomasWalpole 的建议后,我删除了在 capybara.rb 中设置 instance_variable_set 的代码,并将 before(:all) 更改为 before(:each) 以每次加载新会话。数据库清理实际上是罪魁祸首,它删除了我设置用户所需的参考数据。

简而言之,这是我的代码问题,而不是整个水豚设置的问题。