I am having an issue regarding Factory Girl in Rails. I currently have pundit setup am trying to test my user policies yet the factory does not seem to work when in rspec.
Inside the rails console, I can load up the console and type:
user=FactoryGirl.create(:admin_user)
r=user.roles
This works correctly creates a user and the correct associations between a user and a role. However, when the factory is used in rspec- A user is created but not the associated assignment. (I discovered this by using 'pp' inside the specific tests.)
I do not need to create a role, since the roles are set so I am looking them up.
Any ideas?
Models
class User < ApplicationRecord
has_many :assignments, dependent: :destroy
has_many :roles, through: :assignments, dependent: :destroy
def has_role?(roles)
roles.each do |role|
if self.roles.include? role
return true
end
end
false
end
class Role < ApplicationRecord
# Associations
has_many :assignments
has_many :users, through: :assignments
class Assignment < ApplicationRecord
# Associations
belongs_to :user
belongs_to :role
Factories
FactoryGirl.define do
factory :user do
first_name {Faker::Name.first_name}
last_name {Faker::Name.last_name}
email {Faker::Internet.email}
password {Faker::Internet.password(8)}
factory :admin_user do
after(:create) do |user|
Assignment.create(user: user , role: Role.find_by(label:'System Admin') )
end
end
end
end
Tests
User Policy Test
describe UserPolicy do
subject { UserPolicy }
let (:current_user) { FactoryGirl.build_stubbed :user}
let (:other_user) { FactoryGirl.build_stubbed :user }
let (:admin) { FactoryGirl.build_stubbed :admin_user}
permissions :index? do
it "denies access if not an admin" do
expect(UserPolicy).not_to permit(current_user)
end
it "allows access for an admin" do
expect(UserPolicy).to permit(admin)
end
end
end
Other Test With same Issue
feature 'User index page', :devise do
after(:each) do
Warden.test_reset!
end
scenario 'user sees own email address' do
user = FactoryGirl.create(:admin_user)
expect(user.has_role?(Role.where(label: 'System Admin'))).to eq true
login_as(user, scope: :user)
visit users_path
expect(page).to have_content user.email
end
end
This test fails since the user has no role assigned.
Controller
class AssignmentsController < ApplicationController
def create
#assignment = Assignment.new(assignment_params)
if #assignment.save
redirect_to users_path(), :notice => "Role Added"
else
flash[:alert]="Unable to Add Role"
end
end
Related
I'm trying to test the 'destroy' action for my nested comments controller.
In my filmweb app I have scope and validations which prevents users from deleting a comment which is not the author. In web version everything works well but I don't know how to test this case.
Here is my comments_controller
def destroy
#comment = #movie.comments.find(params[:id])
if #comment.destroy
flash[:notice] = 'Comment successfully deleted'
else
flash[:alert] = 'You are not the author of this comment'
end
redirect_to #movie
end
Comment model
class Comment < ApplicationRecord
belongs_to :user
belongs_to :movie
validates :body, presence: true
validates :user, :movie, presence: true
validates :user, uniqueness: { scope: :movie }
scope :persisted, -> { where.not(id: nil) }
end
User model has_many :comments, dependent: :destroy Movie model has_many :comments, dependent: :destroy .
I'm using devise and FactoryBot, specs are here:
describe "DELETE #destroy" do
let(:user) { FactoryBot.create(:user) }
let(:movie) { FactoryBot.create(:movie) }
let(:other_user) { FactoryBot.create(:user, user_id: 100)}
it "doesn't delete comment" do
sign_in(other_user)
comment = FactoryBot.create(:comment, movie: movie, user: user)
expect do
delete :destroy, params: { id: comment.id, movie_id: movie.id }
end.to_not change(Comment, :count)
expect(flash[:alert]).to eq "You are not the author of this comment"
end
end
I've got an error undefined method `user_id=' for #<User:0x00007fb049644d20> and no idea what is the good way to do so.
===EDIT===
Here is my FactoryBot
FactoryBot.define do
factory :user do
email { Faker::Internet.email }
password "password"
confirmed_at 1.day.ago
end
factory :unconfirmed_user do
email { Faker::Internet.email }
password "password"
end
end
The problem is that the the users table does not have a user_id column which you are trying to use in the other_user instance, the column name is simply id:
let(:other_user) { FactoryBot.create :user, id: 100 }
You can leave out the id entirely, it will get a different id automatically:
let(:other_user) { FactoryBot.create :user }
In our Rails app, we have the following models:
class User < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :calendars, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
class Calendar < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :users, through: :administrations
end
We tried to validate the Administration model with the following administration_test.rb test file:
require 'test_helper'
class AdministrationTest < ActiveSupport::TestCase
def setup
#user = users(:noemie)
#administration = Administration.new(user_id: #user.id, calendar_id: #calendar_id)
end
test "should be valid" do
assert #administration.valid?
end
test "user id should be present" do
#administration.user_id = nil
assert_not #administration.valid?
end
test "calendar id should be present" do
#administration.calendar_id = nil
assert_not #administration.valid?
end
end
When we run the test, we get the following results:
FAIL["test_calendar_id_should_be_present", AdministrationTest, 2015-06-30 07:24:58 -0700]
test_calendar_id_should_be_present#AdministrationTest (1435674298.26s)
Expected true to be nil or false
test/models/administration_test.rb:21:in `block in <class:AdministrationTest>'
FAIL["test_user_id_should_be_present", AdministrationTest, 2015-06-30 07:24:58 -0700]
test_user_id_should_be_present#AdministrationTest (1435674298.27s)
Expected true to be nil or false
test/models/administration_test.rb:16:in `block in <class:AdministrationTest>'
We are kind of lost: is this the right way to right the test?
If no, how should we write it?
If yes, how can we make it pass?
The problem is not your test but rather that you are expecting the wrong outcome.
belongs_toin ActiveRecord does not add a validation, the macro simply creates a relation.
To validate a relation you would use validates_associated which calls #valid? on each of the associated records and validates_presence_of to ensure that the associated record is present.
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
validates_associated :user
validates :user, presence: true
end
When testing validations it is better to write on the assertions on the errors hash, as assert_not #administration.valid? will give a false positive if the validation fails for any other reason.
Bad:
test "user id should be present" do
#administration.user_id = nil
assert_not #administration.valid?
end
Good:
test "user id should be present" do
#administration.user_id = nil
#administration.valid?
assert #administration.errors.key?(:user)
end
Ok, we found the solution.
We updated our Administration model as follows:
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
validates :user_id, presence: true
validates :calendar_id, presence: true
end
And also edited our administration_test.rb file:
require 'test_helper'
class AdministrationTest < ActiveSupport::TestCase
def setup
#user = users(:noemie)
#calendar = calendars(:one)
# This code is not idiomatically correct.
#administration = Administration.new(user_id: #user.id, calendar_id: #calendar.id)
end
test "should be valid" do
assert #administration.valid?
end
test "user id should be present" do
#administration.user_id = nil
assert_not #administration.valid?
end
test "calendar id should be present" do
#administration.calendar_id = nil
assert_not #administration.valid?
end
end
The tests are now passing just fine.
There are different kinds of users in my system. One kind is, let's say, a designer:
class Designer < ActiveRecord::Base
attr_accessible :user_id, :portfolio_id, :some_designer_specific_field
belongs_to :user
belongs_to :portfolio
end
That is created immediately when the user signs up. So when a user fills out the sign_up form, a Devise User is created along with this Designer object with its user_id set to the new User that was created. It's easy enough if I have access to the code of the controller. But with Devise, I don't have access to this registration controller.
What's the proper way to create a User and Designer upon registration?
In a recent project I've used the form object pattern to create both a Devise user and a company in one step. This involves bypassing Devise's RegistrationsController and creating your own SignupsController.
# config/routes.rb
# Signups
get 'signup' => 'signups#new', as: :new_signup
post 'signup' => 'signups#create', as: :signups
# app/controllers/signups_controller.rb
class SignupsController < ApplicationController
def new
#signup = Signup.new
end
def create
#signup = Signup.new(params[:signup])
if #signup.save
sign_in #signup.user
redirect_to projects_path, notice: 'You signed up successfully.'
else
render action: :new
end
end
end
The referenced signup model is defined as a form object.
# app/models/signup.rb
# The signup class is a form object class that helps with
# creating a user, account and project all in one step and form
class Signup
# Available in Rails 4
include ActiveModel::Model
attr_reader :user
attr_reader :account
attr_reader :membership
attr_accessor :name
attr_accessor :company_name
attr_accessor :email
attr_accessor :password
validates :name, :company_name, :email, :password, presence: true
def save
# Validate signup object
return false unless valid?
delegate_attributes_for_user
delegate_attributes_for_account
delegate_errors_for_user unless #user.valid?
delegate_errors_for_account unless #account.valid?
# Have any errors been added by validating user and account?
if !errors.any?
persist!
true
else
false
end
end
private
def delegate_attributes_for_user
#user = User.new do |user|
user.name = name
user.email = email
user.password = password
user.password_confirmation = password
end
end
def delegate_attributes_for_account
#account = Account.new do |account|
account.name = company_name
end
end
def delegate_errors_for_user
errors.add(:name, #user.errors[:name].first) if #user.errors[:name].present?
errors.add(:email, #user.errors[:email].first) if #user.errors[:email].present?
errors.add(:password, #user.errors[:password].first) if #user.errors[:password].present?
end
def delegate_errors_for_account
errors.add(:company_name, #account.errors[:name].first) if #account.errors[:name].present?
end
def persist!
#user.save!
#account.save!
create_admin_membership
end
def create_admin_membership
#membership = Membership.create! do |membership|
membership.user = #user
membership.account = #account
membership.admin = true
end
end
end
An excellent read on form objects (and source for my work) is this CodeClimate blog post on Refactoring.
In all, I prefer this approach vastly over using accepts_nested_attributes_for, though there might be even greater ways out there. Let me know if you find one!
===
Edit: Added the referenced models and their associations for better understanding.
class User < ActiveRecord::Base
# Memberships and accounts
has_many :memberships
has_many :accounts, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :account
end
class Account < ActiveRecord::Base
# Memberships and members
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
has_many :admins, through: :memberships,
source: :user,
conditions: { 'memberships.admin' => true }
has_many :non_admins, through: :memberships,
source: :user,
conditions: { 'memberships.admin' => false }
end
This structure in the model is modeled alongside saucy, a gem by thoughtbot. The source is not on Github AFAIK, but can extract it from the gem. I've been learning a lot by remodeling it.
If you don't want to change the registration controller, one way is to use the ActiveRecord callbacks
class User < ActiveRecord::Base
after_create :create_designer
private
def create_designer
Designer.create(user_id: self.id)
end
end
I have implemented cancan and would like to test abilities as recommended on the cancan wiki. I trying to replicate "user can only destroy projects which he owns."
spec/models/ability_spec.rb:
require "cancan/matchers"
require 'spec_helper'
describe Ability do
context "user is investigator" do
it "user can only destroy projects which he owns" do
user = FactoryGirl.create(:user)
ability = Ability.new(user)
ability.should be_able_to(:destroy, Project.new(:user => user))
end
end
end
However I get:
ActiveModel::MassAssignmentSecurity::Error:
Can't mass-assign protected attributes: user
Models:
class User < ActiveRecord::Base
has_many :projects, dependent: :destroy
devise :database_authenticatable, etc...
attr_accessible :email, :password, :password_confirmation, :remember_me, :locale
validates :role, :presence => true
end
class Project < ActiveRecord::Base
belongs_to :user
end
Factory:
FactoryGirl.define do
factory :user do |f|
f.email { Faker::Internet.email }
f.password "secret"
f.role 1
end
end
I understand why this error arrises, and have tried various ways round it, but don't have a good enough understanding of factories to crack it. Can you help?
So the problem was related to not using Factory Girl when creating the project. It should have been:
describe Ability do
context "user is investigator" do
it "user can only destroy projects which he owns" do
user = FactoryGirl.create(:user)
ability = Ability.new(user)
ability.should be_able_to(:destroy, FactoryGirl.create(:project, :user => user))
end
end
end
Models:
class User < ActiveRecord:Base
has_many :roles
has_many :networks, :through => :roles
end
class Network < ActiveRecord:Base
has_many :roles
has_many :network, :through => :roles
end
class Role < ActiveRecord:Base
attr_accesible :user_id, :network_id, :position
belongs_to :user
belongs_to :network
end
The default for role is "member"
In the console I can type:
> #role = Role.find(1)
> #role.position
=> "member"
But in my Rspec tests, I use FactoryGirl to create a user, network, and role. And I have the test #role.should respond_to(:position) I have also tried just assigning it #role.position = "admin". And no matter what, I get an error like:
Failure/Error: #role.should respond_to(:position)
expected [#<Role id:1, user_id: 1, position: "member", created_at...updated_at...>] to respond to :position
Am I missing something very basic?
EDIT:
factories.rb
FactoryGirl.define do
factory :user do
name "Example User"
sequence(:email) {|n| "email#{n}#program.com"}
end
factory :network do
sequence(:name) {|n| "Example Network #{n}"}
location "Anywhere, USA"
description "Lorem Ipsum"
end
factory :role do
association :user
association :network
position "member"
end
end
network_controller_spec
...
before(:each) do
#user = test_sign_in(FactoryGirl.create(:user)
#network = FactoryGirl.create(:network)
#role = FactoryGirl.create(:role, :user_id => #user.id, :network_id = #network.id)
#I have also tried without using (_id) I have tried not setting the position in the factories as well.
end
it "should respond to position" do
get :show, :id => #network
# This may not be the best or even correct way to find this. But there should only be one, and this method works in the console.
#role = Role.where(:user_id => #user.id, :network_id => #network.id)
#role.should respond_to(:position)
end
Jesse is correct in his comment, hopefully he will come back and write it as an answer, in the meantime, the code should be:
#role = Role.where(:user_id => #user.id, :network_id => #network.id).first
or
#role = Role.find_by_user_id_and_network_id(#user.id, #network.id)
As an aside, it seems a little odd to be testing the role class in the network controller spec (unless this is just an exploratory test to work out why things aren't working as expected).