[英]RSpec with multi tenancy - Why is this simple test failing?
我在做什么
我最近以“带范围的多租户” (需要订阅)为指导,实现了多租户(使用范围)。 注意:我正在使用可怕的“ default_scope”来进行租户作用域确定(如Ryan的Railscast中所示)。 一切都可以在浏览器中正常工作,但是我的很多(不是全部)测试都失败了,我不知道为什么。
我从头开始构建了身份验证(基于此Railscast:从头开始进行身份验证(修订) -需要订阅),并使用auth_token来实现“记住我”功能(基于此Railscast: 记住我并重置密码 )。
我的问题
为什么此测试失败,以及两个变通办法为何起作用? 我已经难过了几天,只是想不通。
我认为正在发生的事情
我正在调用Jobs#create操作,并且Job.count 减少 1而不是增加 1。我认为正在创建的是工作,然后应用程序丢失了“承租人”分配(承租人正在删除设为零),并且测试正在计算乔布斯的错误租户。
奇怪的是,它期望得到“ 1”并得到“ -1”(而不是“ 0”),这意味着它得到了计数(请注意,在before块中已经创建了一个“种子”作业,因此它很可能在计数“ 1”,然后调用#create),调用create动作( 应该使总数增加1到2),然后失去租户,并切换到零个有0个工作的租户。 所以:
...导致Job.count更改为-1。
您可以在下面看到我通过在测试中的Job.count行中添加“ .unscoped”来半确认这一点。 这意味着存在预期的工作数量,但是这些工作不在应用程序测试的租户中。
我不明白的是如何失去租户。
码
我尝试获取代码的相关部分,并创建了专用的单项测试规范,以使其尽可能易于剖析。 如果我可以采取其他措施来简化可能的回答者操作,请告诉我该怎么做!
# 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并通过检查会话哈希,那么我会一直看到相同的租户ID:
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
我认为不是RSpec忽略了默认范围,而是通过将当前用户设置为nil在环境过滤器的ApplicationController中对其进行了重置。
我在使用Assigns(...)时遇到了这个问题,它的发生是因为在评估分配时,该关系实际上已解决。 我认为您的期望也可能是这种情况。
更新:在我的情况下,我能找到的最干净的解决方案(尽管我仍然讨厌它)是通过在测试环境中不将当前用户设置为nil来泄漏默认范围。
在您的情况下,总计为:
def scope_current_tenant
update_current_tenant
yield
ensure
Tenant.current_id = nil unless Rails.env == 'test'
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)
}.to change(Job.where(tenant_id: @tenant1.id),:count).by(1)
end
end
end
我变了:
change(Job,:count).by(1)
...至:
change(Job.where(tenant_id: @tenant1.id),:count).by(1)
注意: @ tenant1是已登录管理员的租户。
我假设default_scopes将在RSpec中应用,但似乎没有(或至少不在“ expect”块的“:change”部分中)。 在这种情况下,Job的default_scope为:
default_scope { where(tenant_id: Tenant.current_id) }
实际上,如果我将该行更改为:
change(Job.where(tenant_id: Tenant.current_id),:count).by(1)
...它也会通过。 因此,如果我在规范中明确模仿Job的default_scope,它将通过。 这似乎证明RSpec忽略了Jobs上的default_scope。
在某种程度上,我认为我的新测试是确保租户数据保持隔离的更好方法,因为我正在显式检查特定租户内的计数,而不是隐式检查租户的计数(通过假定计数位于“当前”承租人”)。
我标记我的答案是正确的,因为它是唯一的答案,如果其他人遇到此问题,我认为我的答案将帮助他们解决问题。 就是说,我真的没有回答我最初的问题,即为什么测试失败。 如果有人对RSpec为何似乎忽略“ expect”块中的default_scope有所了解,则可能有助于使该问题对其他人有用。
你们有同样的问题。 我没有做出让自己感到满意的方式来解决问题,但仍然比验证您的RAILS_ENV好。 举这个例子。
it "saves person" do
expect {
some_post_action
}.to change(Person, :count).by(1)
end
每次我尝试保存count方法时,都会进行如下选择:“从tenant_id为null的人中选择count(*)”
我设法通过在更改方法中设置Person.unscoped来解决此问题:
}.to change(Person, :count).by(1)
对此:
}.to change(Person.unscoped, :count).by(1)
这不是最好的解决方案,但我仍在尝试寻找一种方法来绕过default_scope。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.