I'm working for a multi-tenant application using Ruby on Rails, Devise, Stripe and milia.
Basic Concept: At first a user create an account by procedural way like using devise sign_up page and create an organization this user has the ability to add multiple members for this organization at this point first user is organization admin and others member/members are the just members like there has no writing permission just reading permission.
At second the first user an send an invitation to a member who will have an organization member, now everything is working like registration, payment and invite to member email and join this member.
I have tried like this
on the user.rb
before_save {self.is_admin = true}
But this saving for all including members.
This is my registration code
class RegistrationsController < Milia::RegistrationsController
skip_before_action :authenticate_tenant!, :only => [:new, :create, :cancel]
def create
# have a working copy of the params in case Tenant callbacks
# make any changes
tenant_params = sign_up_params_tenant
user_params = sign_up_params_user
coupon_params = sign_up_params_coupon
sign_out_session!
# next two lines prep signup view parameters
prep_signup_view( tenant_params, user_params, coupon_params )
# validate recaptcha first unless not enabled
if !::Milia.use_recaptcha || verify_recaptcha
Tenant.transaction do
#tenant = Tenant.create_new_tenant( tenant_params, user_params, coupon_params)
if #tenant.errors.empty? # tenant created
if #tenant.plan == 'premium'
#payment = Payment.new({email: user_params["email"],
token: params[:payment]["token"],
tenant: #tenant
})
flash[:error] = "Please check registration errors" unless #payment.valid?
begin
#payment.process_payment
#payment.save
rescue Exception => e
flash[:error] = e.message
#tenant.destroy
log_action('Payment Failed')
render :new and return
end
end
else
resource.valid?
log_action( "tenant create failed", #tenant )
render :new
end # if .. then .. else no tenant errors
if flash[:error].blank? || flash[:error].empty?
initiate_tenant( #tenant ) # first time stuff for new tenant
devise_create( user_params ) # devise resource(user) creation; sets resource
if resource.errors.empty? # SUCCESS!
log_action( "signup user/tenant success", resource )
# do any needed tenant initial setup
Tenant.tenant_signup(resource, #tenant, coupon_params)
else # user creation failed; force tenant rollback
log_action( "signup user create failed", resource )
raise ActiveRecord::Rollback # force the tenant transaction to be rolled back
end # if..then..else for valid user creation
else
resource.valid?
log_action('Payment proccesing fails', #tenant)
render :new and return
end # if. . then .. else no tenant errors
end # wrap tenant/user creation in a transaction
else
flash[:error] = "Recaptcha codes didn't match; please try again"
# all validation errors are passed when the sign_up form is re-rendered
resource.valid?
#tenant.valid?
log_action( "recaptcha failed", resource )
render :new
end
end # def create
end
My question is: How to create is_admin: true for who create organization?
Thanks!
If I understood your concept like you need to assign is_admin: true for who can register using this registration controller, Right? If yes then it's very easy update this user_params = sign_up_params_user line of code
Try the following
user_params = sign_up_params_user.merge({ is_admin: true })
Now assign is_admin: true for only who can create an account with an organization.
Now if you block to special permission for normal members then create a method to user.rb file like
def is_admin?
is_admin
end
Then for permission
if current_user.is_admin?
#=> Permission for admin
else
#=> Permission denied for normal members
end
Hope it helps
If using something like rolify gem, you can add the line
#user.add_role(:admin)
in the following context in registrations_controller.rb`:
Tenant.transaction do
#tenant = Tenant.create_new_tenant( tenant_params, user_params, coupon_params)
if #tenant.errors.empty? # tenant created
initiate_tenant( #tenant ) # first time stuff for new tenant
devise_create( user_params ) # devise resource(user) creation; sets resource
#user.add_role(:admin)
Related
I am trying to learn how to use Rails 5 (generally) but specifically, I'm trying to learn how to use service classes.
I'm trying to write a service class that maps a user's given email address (user's have an attribute called :email) to organisation's domain names. Organisations have attributes called :email_format. I use that attribute to hold the part of the email address that follows the "#".
When a user creates an account, I want to take their email address that they use to sign up with, and match the bit after the # to each of the organisations that I know about and try to find a matching one.
My attempts at this are plainly wrong, but I'm struggling to figure out why.
I have resources called User, Organisation and OrgRequest. The associations are:
User
belongs_to :organisation, optional: true
has_one :org_request
Organisation
has_many :org_requests
has_many :users
OrgRequest
belongs_to :user
belongs_to :organisation
I have tried to write a service class as:
class User::OrganisationMapperService #< ActiveRecord::Base
def self.call(user: u)
new(user: user).call
end
def initialize(user: u)
self.user = user
end
def call
if matching_organisation.present?
# user.organisation_request.new(organisation_id: matching_organisation.id)
# user.update_attributes!(organisation_id: matching_organisation.id)
else
#SystemMailer.unmatched_organisation(user: user).deliver_now
end
end
private
attr_accessor :user
def matching_organisation
# User::OrganisationMapperService.new(user).matching_organisation
User::OrganisationMapperService.new(user: user)
end
end
I then have an org requests controller with:
class Users::OrgRequestsController < ApplicationController
before_action :authenticate_user!, except: [:new, :create, :requested]
before_action :set_org_request, only: [:approved, :rejected, :removed]
# skip_before_action :redirect_for_unrequested_organisation
# skip_before_action :redirect_for_unknown_organisation
def index
organisation = Organisation.find_by(owner_id: current_user.id)
return redirect_to(user_path(current_user.id)) if organisation.nil?
#org_requests = organisation.org_requests
end
def new
#all_organisations = Organisation.select(:title, :id).map { |org| [org.title, org.id] }
#org_request = OrgRequest.new#form(OrganisationRequest::Create)
matched_organisation = User::OrganisationMapperService.new(current_user).matching_organisation
#org_request.organisation_id = matched_organisation.try(:id)
end
def create
#org_request = OrgRequest.new(org_request_params)
#org_request.user_id = current_user.id
if #org_request.save
OrgRequest::ProcessService.new(org_request).process
return redirect_to(user_path(current_user),
flash[:alert] => 'Your request is being processed.')
else
# Failure scenario below
#all_organisations = Organisation.select(:title, :id).map { |org| [org.title, org.id] }
render :new
end
end
def requested
# Need help - if this is contained in form inputs - how do i stop from overriding the submit path?
redirect_to(user_path(current_user))
#not sure about this - a similar redirect isnt required for articles or project create
end
def approve
#org_request = current_user.organisation.org_requests.find(params[:id])
if #org_request.state_machine.transition_to!(:approved)
flash[:notice] = "You've added this member."
redirect_to org_requests_path
else
flash[:error] = "You're not able to manage this organisation's members"
redirect_to :index
end
end
def remove
#org_request = current_user.organisation.org_requests.find(params[:id])
if #org_request.state_machine.transition_to!(:removed)
flash[:notice] = "Removed from the organisation."
redirect_to action: :index
# format.html { redirect_to :index }
# format.json { render :show, status: :ok, location: #project }
# redirect_to action: :show, id: project_id
# add mailer to send message to article owner that article has been approved
else
flash[:error] = "You're not able to manage this organisation's members"
redirect_to(user_path(current_user))
# redirect_to action: :show, id: project_id
end
end
def decline
#org_request = current_user.organisation.org_requests.find(params[:id])
if #org_request.state_machine.transition_to!(:declined)
flash[:notice] = "You're not eligible to join this organisation"
redirect_to action: :index
# redirect_back(fallback_location: root_path)
# format.html { redirect_to :index }
# redirect_to action: :show, id: organisation_request.profile
# add mailer to send message to article owner that article has been approved
else
flash[:error] = "You're not able to manage this organisation's members"
redirect_to(user_path(current_user))
# redirect_to action: :show, id: organisation_request.profile
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_org_request
#org_request = OrgRequest.find(params[:id])
authorize #org_request
end
# Never trust parameters from the scary internet, only allow the white list through.
def org_request_params
params.require(:org_request).permit(:organisation_id, :name) # Need help - not sure if I need to put user id and organisation id in this permission
end
end
I can't figure out another approach to this. When I try this, I get this error:
wrong number of arguments (given 1, expected 0)
The error message highlights line 7 of my service class, which has:
def initialize(user: u)
self.user = user
end
I have previously asked questions about this problem here: superclass mismatch for class User - inheriting from ActiveRecord::Base
but I haven't managed to catch the drift of the advice or what is causing the problem. This attempt is a mash up of suggestions that I have gleaned from at least 10 different tutorials - so I appreciate that its highly unlikely to be correct, but I'm struggling to understand how the different parts of this work to know what to try differently.
Can anyone give me a steer on how to try to progress this attempt?
Organisation mapper decorator has:
class User < ActiveRecord::Base
class OrganisationMapper < ::ApplicationDecorator
def matching_organisation
#matching_organisation ||= Organisation.by_email_format(email_format).first
end
def email_format
user.email.split('#').last
end
private
def user
#model
end
end
end
Application decorator has:
class ApplicationDecorator
def initialize(model)
#model = model
end
private
def method_missing(method, *args)
args.empty? ? #model.send(method) : #model.send(method, *args)
end
end
Org request service class has:
class OrgRequest::CreateService < ActiveRecord::Base
attr_accessor :org_request
def self.call(user_id: user_id, organisation_id: org_id)
new(user_id: user_id, organisation_id: organisation_id).call
end
def initialize(user_id: user_id, organisation_id: org_id)
self.user_id = user_id
self.organisation_id = organisation_id
end
def call
self.org_request \
= OrgRequest.new(user_id: current_user.id,
organisation_id: params[:org_request][:organisation_id])
if org_request.save
# send the email
true
else
false
end
end
end
NEXT ATTEMPT
I have tried every variation on this that I can think of. Nothing I'm trying makes any sense to me but I can't make sense out of any examples that I can find.
My service class currently has:
class User::OrganisationMapperService #< ActiveRecord::Base
def self.call(user: u)
new(user: user).call
end
def initialize(user: u)
self.user = user
end
def call
# if matching_organisation.present?
# user.org_request.new(organisation_id: matching_organisation.id)
# if found create a request for that user to enter the organisation
if match_domain.present?
OrgRequest.create(user: #user, organisation_id: #organisation_domain.organisation.id) #if organisation
# user.update_attributes!(organisation_id: matching_organisation.id)
else
#SystemMailer.unmatched_organisation(user: user).deliver_now
end
end
private
attr_accessor :user
# def matching_organisation
# # User::OrganisationMapperService.new(user).matching_organisation
# User::OrganisationMapperService.new(user: user).Organisation.by_email_format(email_format).first
# end
# def matching_organisation
# #matching_organisation ||= Organisation.by_email_format(email_format).first
# end
def user_domain
user.email.split('#').last
end
def organisation_domain
#organisation = Organisation.find_by(email_format: user_domain)
end
# def user_email_domain
# # extract domain from users email
# user_email_domain = #user.email.split('#').last
# end
def match_domain
return unless #user_domain == #organisation.email_format
end
# find an organisation with a matching domain
# end
end
It's plainly wrong. The error message says:
NameError - undefined local variable or method `organisation' for #<User::OrganisationMapperService:0x007faec6ec06b8>
I can't make sense of the error message either because I have put '#' in front of every instance of 'organisation' just to try to make that error go away. It doesn't.
Please help.
ANOTHER COMPLETELY SENSELESS ERROR MESSAGE
I had another go at trying to write the method to check whether an email domain matches an organisation's email format in my service class.
The call method now has:
def call
if user_domain == Organisation.email_format.any?
OrgRequest.create(user: #user, organisation_id: #organisation_domain.organisation.id) #if organisation
else
end
end
The error message in the console says:
NoMethodError - undefined method `email_format' for #<Class:0x007faec72d8ac0>
That has to be nonsense because my organisation table has an attribute in it called :email_format. In the console, I can write:
o = Organisation.first.email_format
Organisation Load (3.3ms) SELECT "organisations".* FROM "organisations" ORDER BY "organisations"."id" ASC LIMIT $1 [["LIMIT", 1]]
That gives me the result I'm looking for.
I'm trying (to my wits end) to learn how rails communicates. I can't make any sense of any of it.
NEXT ATTEMPT
Next guess of a go at the call method:
def call
if user_domain == organisation_domain?
OrgRequest.create(user: #user, organisation_id: #organisation_domain.organisation.id) #if organisation
else
end
Produces this error:
NoMethodError - undefined method `organisation_domain?' for #<User::OrganisationMapperService:0x007faec3be3600>:
I can't seem to find a single form of expression that doesnt produce this error.
The problem appears to be in the following line:
matched_organisation = User::OrganisationMapperService.new(current_user).matching_organisation
It should be this instead:
matched_organisation = User::OrganisationMapperService.new(user: current_user).matching_organisation
I had a session on code mentor. This is the answer. I hope it might help someone else who is trying to learn.
class User::OrganisationMapperService #< ActiveRecord::Base
def self.call(user: u)
new(user: user).call
end
def initialize(user: u)
self.user = user
end
def call
if organisation_domain.present?
OrgRequest.create(user: #user, organisation_id: organisation_domain.id) #if organisation
else
end
end
private
attr_accessor :user
def user_domain
user.email.split('#').last
end
def organisation_domain
#organisation ||= Organisation.find_by(email_format: user_domain)
end
end
I'm building an app that has 3 models (Customer, Points, Admin). Customer has Points, Points belong to Customer. Then Admin has user_name and password_hash as attributes, storing the passwords via Bcrypt. Once a customer searches themselves via phone number, then their points show up. But to add points, an admin has to log in with just a password (code of 4 digits) then get access to adding points.
I'm having trouble how to find the admin via only a password, not user_name and password.
class AdminsController < ApplicationController
def new
#admin = Admin.new
end
def create
#Admin = Admin.new(admin_params)
if #admin.save
redirect_to root_path
else
flash[:error] = "incorrect data, please check form"
render new_admin_path
end
end
def login
#customer = Customer.find(params[:id])
# Need to get the input password
params[:password]
# Change the inputed password into a password hash
# inputed_password_hash (NEED HELP HERE)
# Compare the password hash with password hashes in the Admin model/database
# to see if it exists.
# if true, send to add points page
# if false, send back to customer page
if Admin.find_by(password_hash: inputed_password_hash)
redirect_to new_points_path
else
render customer_path
end
end
private
def admin_params
params.require(:admin).permit(:user_name, :password, :password_confirmation)
end
end
I've read several SO links on this topic. Even if you can hack it to get current_user in model, you shouldn't do it. So, what are my options in my case?
I'm using the devise_invitable gem, and one of the commands is User.invite!({:email => email}, current_user), which stores who the user is invited by (current_user). I'd like to have this information.
Currently, users are invited to join a private group, and this process is handled in my group.rb model:
# group.rb
def user_emails
end
def user_emails=(emails_string)
emails_string = emails_string.split(%r{,\s*})
emails_string.each do |email|
user = User.find_for_authentication(email: email)
if user
self.add user
GroupMailer.welcome_email(user)
else
User.invite!(email: email) # But I want this: User.invite!({:email => email}, current_user)
user = User.order('created_at ASC').last
self.add user
end
end
end
If relevant, it's just a text_area that receives these emails to process:
# groups/_form.html.erb
<%= f.text_area :user_emails, rows: 4, placeholder: 'Enter email addresses here, separated by comma', class: 'form-control' %>
Without having to re-arrange too much, how can I run User.invite!({:email => email}, current_user) in this process, so that this useful information (who is invited by whom) is stored in my database? Much thanks!
Update:
With #Mohamad's help below, I got it working.
# group.rb
def emails
end
def invite_many(emails, inviter)
emails.split(%r{,\s*}).each do |email|
if user = User.find_for_authentication(email: email)
add user
GroupMailer.group_invite user
else
add User.invite!({:email => email}, inviter)
end
end
end
# groups_controller.rb
def update
#group = Group.friendly.find(params[:id])
if #group.update_attributes(group_params)
emails = params[:group][:emails]
#group.invite_many(emails, current_user) # also put this in #create
redirect_to #group
else
flash[:error] = "Error saving group. Please try again."
render :edit
end
end
And then nothing in my User model because User.invite is defined already by devise_invitable and I didn't need to do anything else. This process is working great now!
There are some subtle issues with your code. There's a potential race condition on the else branch of your code where you try to add the last created user. I'm also unsure that you need a setter method here unless you are access emails from elsewhere in the instance of Group.
As suggested by others, pass the current user as an argument form the controller. I'm not sure how invite! is implemented, but assuming it returns a user, you can refactor your code considerably.
I would do somethng like this:
def invite_many(emails, inviter)
emails.split(%r{,\s*}).each do |email|
if user = User.find_for_authentication(email: email)
add user
GroupMailer.welcome_email user
else
add User.invite!(email, inviter)
end
end
end
# controller
#group.invite_many(emails, current_user)
# User.invite
def invite(email, inviter)
# create and return the user here, and what else is necessary
end
If you are calling user_emails() from the controller (and I'm guessing you are as that must be where you are receiving the form to pass in emails_string), you can pass in the current_user:
user_emails(emails_string, current_user)
and change user_emails to receive it:
def user_emails=(emails_string, current_user)
You can store the current_user with global scope ,like #current_user,which can be assigned in sessions controller,so in model you will just #current_user as the current user of the app.
I am using has_secure_password with a rails 4.1.5 app. I wanted to decouple my login functionality from my SessionsController so I can reuse it to login any user from wherever I want in my app - for example logging in a user after registration, logging analytics events etc.
So I refactored my code into a LoginUser service object and I am happy with it.
The problem is that my controller still has some coupled logic after this refactoring. I am using a Form Object (via the reform gem) for form validation and then passing on the user, session and password to the LoginUser service.
Here is what the create method in my SessionsController looks like:
def create
login_form = Forms::LoginForm.new(User.new)
if login_form.validate(params[:user]) # validate the form
begin #find the user
user = User.find_by!(email: params[:user][:email])
rescue ActiveRecord::RecordNotFound => e
flash.now.alert = 'invalid user credentials'
render :new and return
end
else
flash.now.alert = login_form.errors.full_messages
render :new and return
end
user && login_service = LoginUser.new(user, session, params[:user][:password])
login_service.on(:user_authenticated){ redirect_to root_url, success: "You have logged in" }
login_service.execute
end
Everything is working as expected but the part I am not happy with is the tied up logic between validating the form and then finding the user before sending it to the service object. Also the multiple flash alerts feel..well..not right.
How would I make this method better by decoupling these two? It seems right now that one is carrying the other on it's back.
For your reference here is my LoginUser service object
class LoginUser
include Wisper::Publisher
attr_reader :user, :password
attr_accessor :session
def initialize(user, session, password)
#user = user
#session = session
#password = password
end
def execute
if user.authenticate(password)
session[:user_id] = user.id
publish(:user_authenticated, user)
else
publish(:user_login_failed)
end
end
end
What sticks out to me the most here is that create is a method with multiple responsibilities that can/should be isolated.
The responsibilities I see are:
validate the form
find the user
return validation error messages
return unknown user error messages
create LoginService object, setup after-auth behavior and do auth
The design goal to clean this up would be to write methods with a single responsibility and to have dependencies injected where possible.
Ignoring the UserService object, my first shot at a refactor might look like this:
def create
validate_form(user_params); return if performed?
user = find_user_for_authentication(user_params); return if performed?
login_service = LoginUser.new(user, session, user_params[:password])
login_service.on(:user_authenticated){ redirect_to root_url, success: "You have logged in" }
login_service.execute
end
private
def user_params
params[:user]
end
def validate_form(attrs)
login_form = Forms::LoginForm.new(User.new)
unless login_form.validate(attrs)
flash.now.alert = login_form.errors.full_messages
render :new
end
end
def find_user_for_authentication(attrs)
if (user = User.find_by_email(attrs[:email]))
user
else
flash.now.alert = 'invalid user credentials'
render :new
end
end
Of note, the return if performed? conditions will check if a render or redirect_to method has been called. If so, return is called and the create action is finished early to prevent double render/redirect errors.
I think this is a big improvement simply because the responsibilities have been divvied up into a few different methods. And these methods have their dependencies injected, for the most part, so that they can continue to evolve freely in the future as well.
What I'm doing
I recently implemented multi-tenancy (using scopes) following Multitenancy with Scopes (subscription required) as a guide. NOTE: I am using the dreaded "default_scope" for tenant scoping (as shown in Ryan's Railscast). Everything is working in browser just fine, but many (not all) of my tests are failing and I can't figure out why.
I built authentication from scratch (based on this Railscast: Authentication from Scratch (revised) - subscription required) and using an auth_token for "Remember me" functionality (based on this Railscast: Remember Me & Reset Password).
My question
Why is this test failing, and why do the two workarounds work? I've been stumped for a couple days now and just can't figure it out.
What I think is happening
I'm calling the Jobs#create action, and the Job.count is reducing by 1 instead of increasing by 1. I think what's happening is the job is being created, then the app is losing the 'tenant' assignment (tenant is dropping to nil), and the test is counting Jobs for the wrong tenant.
What's odd is that it's expecting "1" and getting "-1" (and not "0"), which implies it's getting a count (note that there's already a 'seed' job created in the before block, so it's probably counting "1" before calling #create), calling the create action (which should increase the count by 1 to 2 total), then losing the tenant and switching to a nil tenant where there are 0 jobs. So it:
Counts 1 (seed job)
Creates a job
Loses the tenant
Counts 0 jobs in the new (probably nil) tenant
...resulting in a -1 change in the Job.count.
You can see below that I've semi-confirmed this by adding ".unscoped" to my Job.count line in the test. This implies that the expected number of jobs is there, but the jobs just aren't in the tenant the app is testing under.
What I don't understand is how it's losing the tenant.
Code
I've tried to grab the relevant parts of my code, and I've created a dedicated single-test spec to make this as easy to dissect as possible. If I can do anything else to make this easy on possible answerers, just let me know what to do!
# 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
Here is the failure from 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
If I add some 'puts' lines to see who the current_tenant is directly and by inspecting the session hash, I see the same tenant ID all the way:
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
Yields...
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
I think it's not that RSpec is ignoring the default scope but it's reset in the ApplicationController in the around filter by setting the current user to nil.
I encountered this issue with assigns(...) and it happened because the relation is actually resolved when you're evaluating assigns. I think this may also be the case with the expectation in your case.
UPDATE: In my situation, the cleanest solution I could find (though I still hate it) is to let the default scope leak through by not setting the current user to nil in test environment.
In your case this would amount to:
def scope_current_tenant
update_current_tenant
yield
ensure
Tenant.current_id = nil unless Rails.env == 'test'
end
I haven't tested it with your code but maybe this will help.
I managed to get my tests to pass, although I'm still not sure why they were failing to begin with. Here's what I did:
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
I changed:
change(Job,:count).by(1)
...to:
change(Job.where(tenant_id: #tenant1.id),:count).by(1)
NOTE: #tenant1 is the logged-in admin's tenant.
I assumed default_scopes would be applied in RSpec, but it seems they aren't (or at least not in the ":change" portion of an "expect" block). In this case, the default_scope for Job is:
default_scope { where(tenant_id: Tenant.current_id) }
In fact, if I change that line to:
change(Job.where(tenant_id: Tenant.current_id),:count).by(1)
...it will also pass. So if I explicitly mimic the default_scope for Job within the spec, it'll pass. This seems like confirmation that RSpec is ignoring my default_scope on Jobs.
In a way, I think my new test is a better way to make sure tenant data stays segregated because I'm explicitly checking counts within a particular tenant rather than implicitly checking the counts for a tenant (by assuming the count is in the "current tenant").
I'm marking my answer is correct because it's the only answer, and if someone else encounters this, I think my answer will help them get past the issue. That said, I really haven't answered my original question regarding why the test was failing. If anyone has any insight into why RSpec seems to be ignoring default_scope in "expect" blocks, that might help making this question useful for others.
I have the same issue of you guys. I didn't resolve in a way that makes me comfortable but is still better than verifying your RAILS_ENV. Take this example.
it "saves person" do
expect {
some_post_action
}.to change(Person, :count).by(1)
end
Every time i try to save the count method makes a select like:
"select count(*) from persons where tenant_id is null"
I manage to resolve this issue by setting Person.unscoped in the change method i changed this:
}.to change(Person, :count).by(1)
to this:
}.to change(Person.unscoped, :count).by(1)
It's not the best solution but i'm still trying to find a way to get around the default_scope.