I would like to test my controller after I added strong_parameters gem, how to do that?
I tried:
Controller
class EventsController < ApplicationController
def update
#event = Event.find(params[:id])
respond_to do |format|
if #event.update_attributes(event_params)
format.html { redirect_to(#event, :notice => 'Saved!') }
else
format.html { render :action => "new" }
end
end
end
private
def event_params
params.require(:event).permit!
end
end
Specs
describe EventsController do
describe "PUT update" do
describe "with forbidden params" do
let(:event) { Event.create! title: "old_title", location: "old_location", starts_at: Date.today }
it "does not update the forbidden params" do
put :update,
id: event.to_param,
event: { 'title' => 'new_title', 'location' => 'NY' }
assigns(:event).title.should eq('new_title') # explicitly permitted
assigns(:event).location.should eq("old_location") # implicitly forbidden
response.should redirect_to event
end
end
end
end
Errors
1) EventsController PUT update with forbidden params does not update the forbidden params
Failure/Error: assigns(:event).title.should eq('new_title') # explicitly permitted
NoMethodError:
undefined method `title' for nil:NilClass
# ./spec/controllers/events_controller_spec.rb:13:in
I see a few things going on here.
The fact that it says undefined method on line 13 is because the #event variable is not being assigned, so assigns(:event) is returning nil.
You should check out why that is happening, maybe you have some authentication that is preventing you from updating the record? Maybe you can check out the testing logs to see what is actually going on.
It could be because you are using let() which is lazy and the record is not actually available yet when you try to search for it, but I'm not completely sure. You could try using let!() and see if that helps.
With regards to the actual usage of strong parameters, if you only want title to be assignable you need to do something like the following:
params.require(:event).permit(:title)
If you use permit!, the event parameters hash and every subhash is whitelisted.
Related
I am testing my controller to ensure that a library class is called and that the functionality works as expected. NB: This might have been asked somewhere else but I need help with my specific problem. I would also love pointers on how best to test for this.
To better explain my problem I will provide context through code.
I have a class in my /Lib folder that does an emission of events(don't mind if you don't understand what that means). The class looks something like this:
class ChangeEmitter < Emitter
def initialize(user, role, ...)
#role = role
#user = user
...
end
def emit(type)
case type
when CREATE
payload = "some payload"
when UPDATE
payload = "some payload"
...
end
send_event(payload, current_user, ...)
end
end
Here is how I am using it in my controller:
class UsersController < ApplicationController
def create
#user = User.new(user_params[:user])
if #user.save
render :json => {:success => true, ...}
else
render :json => {:success => false, ...}
end
ChangeEmitter.new(#user, #user.role, ...).emit(ENUMS::CREATE)
end
end
Sorry if some code doesn't make sense, I am trying to explain the problem without exposing too much code.
Here is what I have tried for my tests:
describe UsersController do
before { set_up_authentication }
describe 'POST #create' do
it "calls the emitter" do
user_params = FactoryGirl.attributes_for(:user)
post :create, user: user_params
expect(response.status).to eq(200)
// Here is the test for the emitter
expect(ChangeEmitter).to receive(:new)
end
end
end
I expect the ChangeEmitter class to receive new since it is called immediately the create action is executed.
Instead, here is the error I get:
(ChangeEmitter (class)).new(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
What am I missing in the above code and why is the class not receiving new. Is there a better way to test the above functionality? Note that this is Rspec. Your help will be much appreciated. Thanks.
You need to put your expect(ChangeEmitter).to receive(:new) code above the post request. When you are expecting a class to receive a method your "expect" statement goes before the call to the controller. It is expecting something to happen in the future. So your test should look something like:
it "calls the emitter" do
user_params = FactoryGirl.attributes_for(:user)
expect(ChangeEmitter).to receive(:new)
post :create, user: user_params
expect(response.status).to eq(200)
end
EDIT
After noticing that you chain the "emit" action after your call to "new" I realized I needed to update my answer for your specific use case. You need to return an object (I usually return a spy or a double) that emit can be called on. For more information on the difference between spies and doubles check out:
https://www.ombulabs.com/blog/rspec/ruby/spy-vs-double-vs-instance-double.html
Basically a spy will accept any method called on it and return itself whereas with a double you have to specify what methods it can accept and what is returned. For your case I think a spy works.
So you want to do this like:
it "calls the emitter" do
user_params = FactoryGirl.attributes_for(:user)
emitter = spy(ChangeEmitter)
expect(ChangeEmitter).to receive(:new).and_return(emitter)
post :create, user: user_params
expect(response.status).to eq(200)
end
I am trying to stub a method in a REST client, but the method isn't being stubbed and always
makes a call to the server. I would like to know why get_additional_info is not being stubbed.
Spec
describe "Test Controller" do
it "will update and redirect to contract" do
RestClientWrapper.any_instance.stub(:get_additional_info).and_return(AdditionalInfo.new({required_info: "..."}))
put :update, {id: 1, bank: {}}, session_user
should redirect_to contract_path
end
end
Controller
def update
additional_info = MyCompany::api.get_additional_info(auth_token,decision.id)
end
MyCompany.rb
def self.api
RestClientWrapper.new
end
I am not sure why what you have doesn't work. But try this
MyCompany.stub(:api).and_return(mock("rest_client_wrapper", :get_additional_info => AdditionalInfo.new({required_info: "..."})))
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.
I have this in my controller spec file
it "should raise 404" do
business = FactoryGirl.build(:business)
expect{get :edit, :id => business}.to raise_error(ActiveRecord::RecordNotFound)
end
if I am right, build does not save to the database, so business should not exist, and my test should pass, but it does not.
I also tried a string as a value of "id", but it still fails.
I have tried with this controller action:
def edit
if params[:id].to_i == 0
name = params[:id].to_s.titleize
#business = Business.find_by_name!(name)
else
#business = Business.find(params[:id])
end
respond_with(#business)
end
an ID that does not exist, and it does indeed show a 404.
If you ask why a condition like that, I also make this action respond to a string for the "id" param.
Any ActiveRecord::RecordNotFound is received by this code in the application controller:
rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
private
def record_not_found
render :text => "404 Not Found Baby!", :status => 404
end
why is my test for a 404 not passing?
Your controller does not raise ActiveRecord::RecordNotFound exception, it rescues from it in ApplicationController. So try to test for response code or text, something like
it "should respond with a 404" do
business = FactoryGirl.build(:business)
get :edit, :id => business
response.response_code.should == 404
end
I know I'm late to the party, but you shouldn't really be creating records in controller tests. You create records in model tests.
In your controller tests, if you are expecting a create to fail, stub it with something like my_model.stub(:save).and_return(false). If you are expecting create to be successful, you could stub it with my_model.stub(:save).and_return(true)
Using Shoulda....
context "record valid" do
before :each do
my_model.stub(:save).and_return(true)
post :create
end
it { should redirect_to(dashboard_url) }
end
Utilizing ActionController's new respond_with method...how does it determine what to render when action (save) is successful and when it's not?
I ask because I'm trying to get a scaffold generated spec (included below) to pass, if only so that I can understand it. The app is working fine but, oddly, it appears to be rendering /carriers (at least that's what the browser's URL says) when a validation fails. Yet, the spec is expecting "new" (and so am I, for that matter) but instead is receiving <"">. If I change the spec to expect "" it still fails.
When it renders /carriers that page shows the error_messages next to the fields that failed validation as one would expect.
Can anyone familiar with respond_with see what's happening here?
#carrier.rb
validates :name, :presence => true
#carriers_controller.rb
class CarriersController < ApplicationController
respond_to :html, :json
...
def new
respond_with(#carrier = Carrier.new)
end
def create
#carrier = Carrier.new(params[:carrier])
flash[:success] = 'Carrier was successfully created.' if #carrier.save
respond_with(#carrier)
end
Spec that's failing:
#carriers_controller_spec.rb
require 'spec_helper'
describe CarriersController do
def mock_carrier(stubs={})
(#mock_carrier ||= mock_model(Carrier).as_null_object).tap do |carrier|
carrier.stub(stubs) unless stubs.empty?
end
end
describe "POST create" do
describe "with invalid params" do
it "re-renders the 'new' template" do
Carrier.stub(:new) { mock_carrier(:save => false) }
post :create, :carrier => {}
response.should render_template("new")
end
end
end
end
with this error:
1) CarriersController POST create with invalid params re-renders the 'new' template
Failure/Error: response.should render_template("new")
expecting <"new"> but rendering with <"">.
Expected block to return true value.
# (eval):2:in `assert_block'
# ./spec/controllers/carriers_controller_spec.rb:81:in `block (4 levels) in <top (required)>'
tl:dr
Add an error hash to the mock:
Carrier.stub(:new) { mock_carrier(:save => false,
:errors => { :anything => "any value (even nil)" })}
This will trigger the desired behavior in respond_with.
What is going on here
Add this after the post :create
response.code.should == "200"
It fails with expected: "200", got: "302". So it is redirecting instead of rendering the new template when it shouldn't. Where is it going? Give it a path we know will fail:
response.should redirect_to("/")
Now it fails with Expected response to be a redirect to <http://test.host/> but was a redirect to <http://test.host/carriers/1001>
The spec is supposed to pass by rendering the new template, which is the normal course of events after the save on the mock Carrier object returns false. Instead respond_with ends up redirecting to show_carrier_path. Which is just plain wrong. But why?
After some digging in the source code, it seems that the controller tries to render 'carriers/create'. There is no such template, so an exception is raised. The rescue block determines the request is a POST and there is nothing in the error hash, upon which the controller redirects to the default resource, which is the mock Carrier.
That is puzzling, since the controller should not assume there is a valid model instance. This is a create after all. At this point I can only surmise that the test environment is somehow taking shortcuts.
So the workaround is to provide a fake error hash. Normally something would be in the hash after save fails, so that kinda makes sense.