Что я делаю
Недавно я внедрил мультиарендность (с использованием областей) после мультитенантности с областями (требуется подписка ) в качестве ориентира. ПРИМЕЧАНИЕ. Я использую ужасную "default_scope" для определения области действия арендатора (как показано в Railscast Райана). В браузере все работает нормально, но многие (не все) мои тесты терпят неудачу, и я не могу понять, почему.
Я создал аутентификацию с нуля (на основе этого Railscast: Аутентификация с нуля (пересмотрено) — требуется подписка) и использование auth_token для функции «Запомнить меня» (на основе этого Railscast: Запомнить меня и сбросить пароль).
Мой вопрос
Почему этот тест не проходит и почему работают два обходных пути? Я уже пару дней в тупике и не могу понять.
Что, по моему мнению, происходит
Я вызываю действие Jobs#create, и Job.count уменьшается на 1, а не увеличивается на 1. Я думаю, что происходит создание задания, затем приложение теряет назначение «клиент» (клиент становится равным нулю), и тест подсчитывает задания для неправильного арендатора.
Что странно, так это то, что он ожидает «1» и получает «-1» (а не «0»), что означает, что он получает счет (обратите внимание, что в блоке «до» уже создано «начальное» задание, так что, вероятно, он считает « 1" перед вызовом #create), вызовом действия create (которое должно увеличить счетчик от 1 до 2), затем потеря клиента и переключение на нулевой клиент, где есть 0 заданий. Так что:
- Считает 1 (начальное задание)
- Создает работу
- Теряет арендатора
- Подсчитывает 0 рабочих мест в новом (вероятно, нулевом) арендаторе
... что приводит к изменению Job.count на -1.
Ниже вы можете видеть, что я частично подтвердил это, добавив «.unscoped» в мою строку Job.count в тесте. Это означает, что ожидаемое количество заданий есть, но их просто нет в клиенте, под которым тестируется приложение.
Чего я не понимаю, так это того, как он теряет арендатора.
Код
Я попытался взять соответствующие части своего кода и создал специальную спецификацию для одного теста, чтобы максимально упростить анализ. Если я могу сделать что-нибудь еще, чтобы облегчить задачу возможным ответчикам, просто дайте мне знать, что делать!
# application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
include SessionsHelper
around_filter :scope_current_tenant
private
def current_user
@current_user ||= User.unscoped.find_by_auth_token!(cookies[:auth_token]) if cookies[:auth_token]
end
helper_method :current_user
def current_tenant
@current_tenant ||= Tenant.find_by_id!(session[:tenant_id]) if session[:tenant_id]
end
helper_method :current_tenant
def update_current_tenant
Tenant.current_id = current_tenant.id if current_tenant
end
helper_method :set_current_tenant
def scope_current_tenant
update_current_tenant
yield
ensure
Tenant.current_id = nil
end
end
# sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.unscoped.authenticate(params[:session][:email], params[:session][:password])
if user && user.active? && user.active_tenants.any?
if params[:remember_me]
cookies.permanent[:auth_token] = user.auth_token
else
cookies[:auth_token] = user.auth_token
end
if !user.default_tenant_id.nil? && (default_tenant = Tenant.find(user.default_tenant_id)) && default_tenant.active
# The user has a default tenant set, and that tenant is active
session[:tenant_id] = default_tenant.id
else
# The user doesn't have a default
session[:tenant_id] = user.active_tenants.first.id
end
redirect_back_or root_path
else
flash.now[:error] = "Invalid email/password combination."
@title = "Sign in"
render 'new'
end
end
def destroy
cookies.delete(:auth_token)
session[:tenant_id] = nil
redirect_to root_path
end
end
# jobs_controller.rb
class JobsController < ApplicationController
before_filter :authenticate_admin
# POST /jobs
# POST /jobs.json
def create
@job = Job.new(params[:job])
@job.creator = current_user
respond_to do |format|
if @job.save
format.html { redirect_to @job, notice: 'Job successfully created.' }
format.json { render json: @job, status: :created, location: @job }
else
flash.now[:error] = 'There was a problem creating the Job.'
format.html { render action: "new" }
format.json { render json: @job.errors, status: :unprocessable_entity }
end
end
end
end
# job.rb
class Job < ActiveRecord::Base
has_ancestry
default_scope { where(tenant_id: Tenant.current_id) }
.
.
.
end
# sessions_helper.rb
module SessionsHelper
require 'bcrypt'
def authenticate_admin
deny_access unless admin_signed_in?
end
def deny_access
store_location
redirect_to signin_path, :notice => "Please sign in to access this page."
end
private
def store_location
session[:return_to] = request.fullpath
end
end
# spec_test_helper.rb
module SpecTestHelper
def test_sign_in(user)
request.cookies[:auth_token] = user.auth_token
session[:tenant_id] = user.default_tenant_id
current_user = user
@current_user = user
end
def current_tenant
@current_tenant ||= Tenant.find_by_id!(session[:tenant_id]) if session[:tenant_id]
end
end
# test_jobs_controller_spec.rb
require 'spec_helper'
describe JobsController do
before do
# This is all just setup to support requirements that the admin is an "Admin" (role)
# That there's a tenant for him to use
# That there are some workdays - a basic requirement for the app - jobs, checklist
# All of this is to satisfy assocations and
@role = FactoryGirl.create(:role)
@role.name = "Admin"
@role.save
@tenant1 = FactoryGirl.create(:tenant)
@tenant2 = FactoryGirl.create(:tenant)
@tenant3 = FactoryGirl.create(:tenant)
Tenant.current_id = @tenant1.id
@user = FactoryGirl.create(:user)
@workday1 = FactoryGirl.create(:workday)
@workday1.name = Time.now.to_date.strftime("%A")
@workday1.save
@checklist1 = FactoryGirl.create(:checklist)
@job = FactoryGirl.create(:job)
@checklist1.jobs << @job
@workday1.checklists << @checklist1
@admin1 = FactoryGirl.create(:user)
@admin1.tenants << @tenant1
@admin1.roles << @role
@admin1.default_tenant_id = @tenant1.id
@admin1.pin = ""
@admin1.save!
# This is above in the spec_test_helper.rb code
test_sign_in(@admin1)
end
describe "POST create" do
context "with valid attributes" do
it "creates a new job" do
expect{ # <-- This is line 33 that's mentioned in the failure below
post :create, job: FactoryGirl.attributes_for(:job)
# This will pass if I change the below to Job.unscoped
# OR it will pass if I add Tenant.current_id = @tenant1.id right here.
# But I shouldn't need to do either of those because
# The tenant should be set by the around_filter in application_controller.rb
# And the default_scope for Job should handle scoping
}.to change(Job,:count).by(1)
end
end
end
end
Вот сбой от rspec:
Failures:
1) JobsController POST create with valid attributes creates a new job
Failure/Error: expect{
count should have been changed by 1, but was changed by -1
# ./spec/controllers/test_jobs_controller_spec.rb:33:in `block (4 levels) in <top (required)>'
Finished in 0.66481 seconds
1 example, 1 failure
Failed examples:
rspec ./spec/controllers/test_jobs_controller_spec.rb:32 # JobsController POST create with valid attributes creates a new job
Если я добавлю несколько строк «puts», чтобы увидеть, кто является current_tenant напрямую, и проверив хэш сеанса, я все время вижу один и тот же идентификатор клиента:
describe "POST create" do
context "with valid attributes" do
it "creates a new job" do
expect{
puts current_tenant.id.to_s
puts session[:tenant_id]
post :create, job: FactoryGirl.attributes_for(:job)
puts current_tenant.id.to_s
puts session[:tenant_id]
}.to change(Job,:count).by(1)
end
end
end
Урожайность...
87
87
87
87
F
Failures:
1) JobsController POST create with valid attributes creates a new job
Failure/Error: expect{
count should have been changed by 1, but was changed by -1
# ./spec/controllers/test_jobs_controller_spec.rb:33:in `block (4 levels) in <top (required)>'
Finished in 0.66581 seconds
1 example, 1 failure
Failed examples:
rspec ./spec/controllers/test_jobs_controller_spec.rb:32 # JobsController POST create with valid attributes creates a new job