Using Rails 5.1.4, Ruby 2.4.1, rspec
Scenario:
In article destroy allow only user current_ma_user with role "a,m"
Then:
Check if current_ma_user.role = "a,m"
or current_ma_user own article (#article.user)
So I create current_ma_user as hash as well as user.
Then call role to check what is user[role ]
Problems:
How to add new method to hash.
How to pass that hash.method from rspec controller_spec to controller.
Failures:
1) ArticlesController DELETE #destroy destroys the requested article
Failure/Error: delete :destroy, params: {id: article.to_param}, session: valid_session, :current_ma_user.role => "a,m"
NoMethodError:
undefined method `role' for :current_ma_user:Symbol
# ./spec/controllers/articles_controller_spec.rb:172:in `block (4 levels) in <top (required)>'
# ./spec/controllers/articles_controller_spec.rb:171:in `block (3 levels) in <top (required)>'
This is the gist
articles_controller_spec.rb:
require 'rails_helper'
RSpec.describe ArticlesController, type: :controller do
class Hash #patch to temp pass problem 1
def role
"a,m" #Hard Code, need to call user["role"] need code
end
end
user = {}
user["uid"] = "admin"
user["provider"] = "Facebook"
user["email"] = "1.0#kul.asia"
user["role"] = "a,m"
current_ma_user = user
describe "DELETE #destroy" do
it "destroys the requested article" do
article = Article.create! valid_attributes
expect {
delete :destroy, params: {id: article.to_param}, session: valid_session
}.to change(Article, :count).by(-1)
end
it "redirects to the articles list" do
article = Article.create! valid_attributes
delete :destroy, params: {id: article.to_param}, session: valid_session
expect(response).to redirect_to(articles_url)
end
end
end
Controller:
class ArticlesController < ApplicationController before_action :load_article, only: [:show, :destroy]
def destroy
if current_ma_user.role.upcase.split(',').include?("A") || current_ma_user == #article.user
#if current_ma_user == #article.user
#article.destroy
end
redirect_to :action=>'index' end
private
def load_article
#article = Article.find(params[:id]) end
end
Updated with line number:
Updated debug to show value of current_ma_user in .spec and controller
This is where your error is coming from (in your controller):
if current_ma_user.role.upcase.split(',').include?("A") || current_ma_user == #article.user
Suggested Solutions
Where is current_ma_user defined in the controller? (if it’s not assigned, then it needs to be assigned before you call the role method on the current_ma_user variable.
Try that and see how it goes.
Do something like this:
current_ma_user = User.find( params[:user_id])
Now you seem to want to pass something into the params hash. Remember to white list whatever you decide to pass into params. Whether it is user id or roles id etc, or a roles string.
When writing your tests, pass in the approrpiate values to the params hash. If you are passing in a user_id in your test, then you will have to make sure that a user is created in the test.
delete :destroy, {:id => article.id.to_s, :user_id => #current_ma_user.id }, session: valid_session
also perhaps in your spec file, in your test, put the current_ma_user in a before filter and make it an instance variable so it will be accessible to all your tests:
before(:each) do
#current_ma_user = user.create( <--- create the user with the
appropriate attributes here --->)
end
Warning: Untested
I just typed it into the stack overflow editor.
Related
I'm new to RSpec and this error is all new to me. Everything seems routine so I can't seem to debug this issue myself. ERROR: expected result to have changed by 1, but was changed by 0. I'll post my code for clarity.
SUBSCRIBER FACTORY:
FactoryGirl.define do
factory :subscriber do
first_name "Tyler"
last_name "Durden"
email "tyler#example.com"
phone_number "8765555"
end
end
CONTROLLER:
class CommentsController < ApplicationController
def new
#comment = Comment.new
end
def create
#subscriber = Subscriber.order('updated_at desc').first
#comment = #subscriber.comments.build(comments_params)
if #comment.save
flash[:notice] = "Thank you!"
redirect_to subscribers_search_path(:comments)
else
render "new"
end
end
private
def comments_params
params.require(:comment).permit(:fav_drink, :subscriber_id)
end
end
SPEC:
require "rails_helper"
describe SubscribersController do
include Devise::TestHelpers
let(:user) { FactoryGirl.create(:user) }
let(:subscriber) { FactoryGirl.attributes_for(:subscriber) }
it "creates a new comment" do
sign_in(user)
comment = FactoryGirl.attributes_for(:comment)
expect { post :create, subscriber: subscriber, comment: comment }.to change{ Comment.count }.by(1)
end
end
ERROR:
Failure/Error: expect { post :create, subscriber: subscriber, comment: comment }.to change{ Comment.count }.by(1)
expected result to have changed by 1, but was changed by 0
# ./spec/controllers/comment_spec.rb:13:in `block (2 levels) in <top (required)>'
Here, you're showing your comments controller, expecting one of its actions to be hit. However, your test case is actually calling the create route of the Subscriptions controller.
When, in your test case, you write describe SubscribersController do, you are establishing a scope for the HTTP requests you make in that block.
So when you call post :create, subscriber: subscriber, comment: comment,
it's the Subscriptions controller which is being hit.
In general, in order to debug, you should
check that the area of code in question is being called
check that values are correct (here, that would mean that the Comment.create object is successfully saved.
Very basic controller spec failing because something with my strong parameters set up is wonky?
class OrdersController
def create
#order = Order.new(order_params)
if #order.valid?
...
end
private
def order_params
params.require(:order).and_permit(:email)
end
end
Test code:
describe OrdersController, "Create action", type: :controller do
it "should call valid? method" do
Order.any_instance.should_receive(:valid?)
post :create, order: {email: "test#example.com"}
end
end
Outcome:
Failure/Error: post :create, order: {email: "test#example.com"}
NoMethodError:
undefined method `and_permit' for {"email"=>"test#example.com"}:ActionController::Parameters
the comment on the original post from jvnill hit the nail on the head, it is permit not and_permit
params.require(:order).permit(:email)
I`ve got these errors while testing ratings_controller.
1) RatingsController create action creates a rating if validations pass
Failure/Error: post :create, rating: {value: 4, user_id: user, hotel_id: hotel}
ActiveRecord::RecordNotFound:
Couldn't find Hotel without an ID
# ./app/controllers/ratings_controller.rb:6:in `create'
# ./spec/controllers/ratings_controller_spec.rb:12:in `block (4 levels) in <top (required)>'
# ./spec/controllers/ratings_controller_spec.rb:11:in `block (3 levels) in <top (required)>'
2) RatingsController create action does not create rating if validations fail
Failure/Error: post :create, rating: {value: 3}
ActiveRecord::RecordNotFound:
Couldn't find Hotel without an ID
# ./app/controllers/ratings_controller.rb:6:in `create'
# ./spec/controllers/ratings_controller_spec.rb:17:in `block (3 levels) in <top (required)>'
3) RatingsController update action updates rating if validations ok
Failure/Error: patch :update, value: 3, user_id: user.id, hotel_id: hotel.id
ActionController::UrlGenerationError:
No route matches {:action=>"update", :controller=>"ratings", :hotel_id=>"1", :user_id=>"2", :value=>"3"}
# ./spec/controllers/ratings_controller_spec.rb:25:in `block (3 levels) in <top (required)>'
I dont know where they come from. Help me please if you can.
My ratings_controller:
class RatingsController < ApplicationController
#before_action :signed_in_user
before_filter :authenticate_user!
def create
#hotel = Hotel.find(params[:hotel_id])
#rating = Rating.new(params[:rating])
#rating.hotel_id = #hotel.id
#rating.user_id = current_user.id
if #rating.save
redirect_to hotel_path(#hotel), :notice => "Your rating has been saved"
end
end
def update
#hotel = Hotel.find(params[:hotel_id])
##rating = current_user.ratings.find(#hotel.id)
#rating = Rating.find_by_hotel_id(#hotel.id)
if #rating.update_attributes(params[:rating])
redirect_to hotel_path(#hotel), :notice => "Your rating has been updated"
end
end
end
My ratings_controller_spec.rb:
require "spec_helper"
describe RatingsController do
let(:rating) { FactoryGirl.create(:rating) }
let(:user) { FactoryGirl.create(:user) }
let(:hotel) { FactoryGirl.create(:hotel) }
describe "create action" do
before { sign_in rating.user }
it "creates a rating if validations pass" do
expect {
post :create, rating: {value: 4, user_id: user, hotel_id: hotel}
}.to change(Rating, :count).by(1)
end
it "does not create rating if validations fail" do
post :create, rating: {value: 3}
expect(response).to redirect_to(hotel_path(hotel))
end
end
describe "update action" do
before { sign_in hotel.user }
it "updates rating if validations ok" do
patch :update, value: 3, user_id: user.id, hotel_id: hotel.id
rating.reload
expect(rating.value).to eq(3);
end
it "updates rating if validations fail" do
end
end
end
Especially third error confusing me because rake routes shows me avaible route to ratings update action.
PATCH /hotels/:id(.:format) hotels#update
PUT /hotels/:id(.:format) hotels#update
Thanks!
Rails - 4.0.8
Ruby - 1.9.3p551
UPDATE 1:
Sorry about routes. My mystake. Just copied the wrong lines.
rating PATCH /ratings/:id(.:format) ratings#update
PUT /ratings/:id(.:format) ratings#update
Seems ok to me and works fine if I start server and test it manually.
About the id problem:
#hotel = Hotel.find(params[:hotel_id]) expects the params hash to have a top-level hotel_id key. When you call the create method from the test, you nest the hotel_id inside the ratings key:
post :create, rating: {value: 4, user_id: user, hotel_id: hotel}
Thus params[:hotel_id] is nil. You need to add the hotel_id as a top-level key:
post :create, rating: {value: 4, user_id: user, hotel_id: hotel}, hotel_id: hotel
Alternatively, you can pass the nested hotel_id directly to the find method:
#hotel = Hotel.find(params[:rating][:hotel_id])
About the route problem:
Your route is mapped to HotelsController (as seen in the rake routes output as hotels#update), but you are testing the RatingsController, so you get the
No route matches {:action=>"update", :controller=>"ratings", :hotel_id=>"1", :user_id=>"2", :value=>"3"}.
Update your routes to route to the RatingsController's update method instead of HotelsController, so you would have a ratings#update route.
Update:
In this case, the route is expecting an id parameter in the url: /ratings/:id, which you are not providing:
patch :update, value: 3, user_id: user.id, hotel_id: hotel.id
Either provide a specific rating id you want to update, or change your routes not to require the id parameter since you are finding the rating through the hotel anyway, so they will look like
patch '/ratings', to 'ratings#update'
instead of
patch '/ratings/:id', to 'ratings#update'.
If you defined your routes with resources :ratings, you can put the specific route above it, so it will be hit first by the router. A good idea would be to exclude it too: resources :ratings, except: :update. Or just use collection routes. See this question for some more info.
I am trying to test to see if posting to a create method in my controller triggers a callback I defined with after_save
Here's the controller method being posted to
def create
#guest = Guest.new(guest_params)
#hotel = Hotel.find(visit_params[:hotel_id])
#set visit local times to UTC
#visit= Visit.new(visit_params)
#visit.checked_out_at = (DateTime.now.utc + visit_params[:checked_out_at].to_i.to_i.days).change(hour: #visit.hotel.checkout_time.hour)
#visit.checked_in_at = Time.now.utc
##visit.user_id = current_user.id
#self_serve = (params[:self_serve] && params[:self_serve] == "true")
if #guest.save
#visit.guest_id = #guest.id
if #visit.save
if #self_serve
flash[:notice] = "#{#visit.guest.name}, you have successfully checked in!."
redirect_to guest_checkin_hotel_path(#visit.hotel)
else
flash[:notice] = "You have successfully checked in #{#visit.guest.name}."
redirect_to hotel_path(#visit.hotel)
end
else
render "new"
end
else
render "new"
end
end
Here's my spec/controllers/guests_controller_spec.rb test that is failing
RSpec.describe GuestsController, :type => :controller do
describe "#create" do
let!(:params) do { name: "John Smith", mobile_number: "9095551234" } end
context "when new guest is saved" do
it "triggers create_check_in_messages callback" do
post :create, params
expect(response).to receive(:create_check_in_messages)
end
end
end
end
Here is my models/concerns/visit_message.rb callback file
module VisitMessage
extend ActiveSupport::Concern
included do
after_save :create_check_in_messages
end
def create_check_in_messages
. . .
end
end
Here is the fail message when I run 'rspec spec/controllers/guests_controller_spec.rb'
1) GuestsController#create when new guest is saved triggers create_check_in_messages callback
Failure/Error: post :create, params
ActionController::ParameterMissing:
param is missing or the value is empty: guest
# ./app/controllers/guests_controller.rb:63:in `guest_params'
# ./app/controllers/guests_controller.rb:10:in `create'
# ./spec/controllers/guests_controller_spec.rb:36:in `block (4 levels) in <top (required)>'
I've been searching all over stackoverflow with no luck. I appreciate any help!
I am assuming that the guest_params method in the controller looks something like this:
def guest_params
params.require(:guest).permit(....)
end
If that is the case, you need to update the POST call in your test case thusly:
post :create, {guest: params}
On a side note, your controller is unnecessarily bloated. I would read up on working with associated models to streamline your code, specifically, using accepts_nested_attributes_for:
http://guides.rubyonrails.org/association_basics.html#detailed-association-reference
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
I have a hard time testing my controller with before_filters, exceptions and some mocking and stubing.
Here is the controller:
before_filter :get_subject, :only => [:show, :edit, :update, :destroy, :update_field]
before_filter :user_has_to_belongs_to_subject_company, :only => [:show, :edit, :update, :destroy, :update_field]
def show
#messages = #subject.get_user_messages(current_user)
end
private
def get_subject
#subject = Subject.find(params[:id])
end
def user_has_to_belongs_to_subject_company
unless #current_user.company.eql?(#subject.company)
raise "Error: current_user does not belongs to subject's company"
end
end
And here is my spec file:
require 'spec_helper'
describe SubjectsController do
describe "for signed users" do
before(:each) do
#current_user = Factory(:user)
sign_in #current_user
end
describe "for user belonging to subject's company" do
before(:each) do
#subject = mock_model(Subject)
Subject.stub!(:find).with(#subject).and_return(#subject)
#current_user.stub_chain(:company, :eql?).and_return(true)
#subject.stub!(:company)
end
it "should not raise an exception" do
expect { get :show, :id => #subject }.to_not raise_error
end
end
describe "for user not belonging to subject's company" do
before(:each) do
#subject = mock_model(Subject)
Subject.stub!(:find).with(#subject).and_return(#subject)
#current_user.stub_chain(:company, :eql?).and_return(false)
#subject.stub!(:company)
end
it "should raise an exception" do
expect { get :show, :id => #subject }.to raise_error
end
end
end
end
And finally, here is the error message:
SubjectsController for signed users for user belonging to subject's company should not raise an exception
Failure/Error: expect { get :show, :id => #subject }.to_not raise_error
expected no Exception, got #<RuntimeError: Error: current_user does not belongs to subject's company>
# ./spec/controllers/subjects_controller_spec.rb:19:in `block (4 levels) in <top (required)>'
Thx for helping!
I'm not seeing the problem, but here's a refactoring suggestion. If you find yourself using more mocks and stubs that usual, maybe it's time to reconsider your interfaces. In this case, you can make your controller skinnier and you model fatter.
# subjects_controller_spec.rb
describe "for user belonging to subject's company" do
before(:each) do
#subject = mock_model(Subject, :verify_user => true)
Subject.stub!(:find).with(#subject).and_return(#subject)
end
# subjects_controller.b
def user_has_to_belongs_to_subject_company
#subject.verify_user(#current_user)
end
# subject.rb
class Subject
def verify_user(user)
unless user.company.eql?(company)
raise "Error: current_user does not belongs to subject's company"
end
What happens if you delete the # in front of #current_user in
def user_has_to_belongs_to_subject_company
unless #current_user.company.eql?(#subject.company)
to get
def user_has_to_belongs_to_subject_company
unless current_user.company.eql?(#subject.company)
And in your specs, do controller.stub!(:current_user).and_return #current_user
I think the problem is one of scope - #current_user in your tests is different to #current_user in your controller. Really depends on how "sign_in #current_user" is implemented.
Also, instead of raising an exception, perhaps your before_filter could redirect the user to another page and set flash[:error]? The before filter is the right place to handle this situation, so it shouldn't raise an exception that would have to be rescued somewhere else (or if not, it would display a 500 page to the user).