Rspec not working with having < operator in controller - ruby-on-rails

I'm following Ryan Bates railscasts on password reseting. I decided to implement his code through TDD but one of my tests refuses to work. Whenever I run
context "for checking update succeeds" do
before do
post :update, id: "2012-12-03 04:23:13 UTC"
end
it { should assign_to(:user) }
it { should respond_with(:redirect) }
it { should redirect_to(signin_path) }
it { should set_the_flash.to("Password has been reset.")}
end
I get the following error
Failure/Error: post :update, id: "2012-12-03 04:23:13 UTC"
NoMethodError:
undefined method `<' for nil:NilClass
My controller is as follows
def update
#user = User.find_by_password_reset_token!(params[:id])
if #user.password_reset_sent_at < 2.hours.ago
redirect_to new_password_reset_path, flash[:error] = "Password reset has expired"
elsif #user.update_attributes(params[:user])
redirect_to root_url, flash[:success] = "Password has been reset."
else
render :edit
end
end
I have to assume I'm writing my test wrong in some form or fashion. How do I fix this? Note that I'm aware the utc time is off date and based on yesterday's time.

Copied from comment:
Seems like #user.password_reset_sent_at is nil. If it is allowed to be nil, you should check by
if #user.password_reset_sent_at && #user.password_reset_sent_at < 2.hours.ago
If it shouldn't be allowed to, you have a bug somewhere in your model. The test seems fine.

Related

How can I test update_attributes function in Rails with RSpec?

In my Rails 4 app I have this update action:
class UsersController < ApplicationController
...
def update
current_email = #user.email
new_email = user_params[:email].downcase
if #user.update_attributes(user_params)
if current_email != new_email
#user.email = current_email
#user.new_email = new_email.downcase
#user.send_email_confirmation_email
flash[:success] = "Please click the link we've just sent you to confirm your new email address."
else
flash[:success] = "User updated."
end
redirect_to edit_user_path(#user)
else
render :edit
end
end
...
end
It basically makes sure that a user cannot simply save any new email address. He will have to confirm it first by clicking on a link in an email we send to him.
This works great, however, for some reason I haven't found a way to test it.
The following RSpec test keeps failing no matter what I do:
it "changes the user's new_email attribute" do
#user = FactoryGirl.create(:user, :email => "john#doe.com")
patch :update, :id => #user, :user => FactoryGirl.attributes_for(:user, :email => "new#email.com")
expect(#user.reload.new_email).to eq("new#email.com")
end
#user.new_email is always nil and the test always fails. What am I missing here?
Re-factoring my update action wouldn't be a problem at all. Maybe there's a better way? Thanks for any help.
I would write the spec like so:
let(:user) { FactoryGirl.create(:user, email: "john#doe.com") }
it "changes the user's new_email attribute" do
expect do
patch :update, id: #user, user: FactoryGirl.attributes_for(:user, email: "new#email.com")
user.reload
end.to change(user, :new_email).from("john#doe.com").to("new#email.com")
end
When it comes to the controller action itself the problem is that the new_email property is never saved to the database, besides that its kind of a mess. You can clean it up by using ActiveRecord::Dirty which tracks attribute changes in the model:
class User < ApplicationRecord
# updates user with attrs but moves a new email to the `new_email`
# column instead
def update_with_email(attrs, &block)
update(attrs) do |record|
if record.email_changed?
record.new_email = record.email.downcase
record.restore_attribute!(:email)
end
# keeps the method signature the same as the normal update
yield record if block_given?
end
end
end
Putting this business logic in the model also lets you test it separatly:
RSpec.describe User, type: :model do
describe "#update_with_email" do
let(:user) { FactoryGirl.create(:user) }
it "does not change the email attribute" do
expect do
user.update_with_email(email: ”xxx#example.com”)
user.reload
end.to_not change(user, :email)
end
it "updates the new_email" do
expect do
user.update_with_email(email: ”xxx#example.com”)
user.reload
end.to change(user, :new_email).to('xxx#example.com')
end
end
end
This lets you keep the controller nice and skinny:
def update
if #user.update_with_email(user_params)
if #user.new_email_changed?
#user.send_email_confirmation_email
flash[:success] = "Please click the link we've just sent you to confirm your new email address."
else
flash[:success] = "User updated."
end
# You probably want to redirect the user away from the form instead.
redirect_to edit_user_path(#user)
else
render :edit
end
end

RSpec ActiveRecord::RecordNotFound Couldn't find User with 'id'=

There are many answered questions about this topic but I can't seem to apply the answers to my issue. I'm getting the following error:
ItemsController POST create redirect to show page
Failure/Error: #user = User.find(params[:user_id])
ActiveRecord::RecordNotFound:
Couldn't find User with 'id'=
I know the reason for the error is because the user_id is nil but I can't seem to figure out the reason why. I'm still fairly new to Rails so excuse me if this is a really easy fix.
RSpec Test
require 'rails_helper'
RSpec.describe ItemsController, type: :controller do
describe "POST create" do
it "redirect to show page" do
post :create, user_id: #user, item: { name: "name"}
expect(response).to redirect_to(user_show_path)
end
end
end
Items Controller
class ItemsController < ApplicationController
def create
#user = User.find(params[:user_id])
#item = Item.new(item_params)
#item.user = current_user
if #item.save
#item = Item.update_items(params[:items])
redirect_to current_user, notice: "Item was saved successfully."
else
flash[:error] = "Error creating item. Please try again."
render user_show_path
end
end
private
def item_params
params.require(:item).permit(:name)
end
end
Routes
user_items POST /users/:user_id/items(.:format) items#create
...and I've implemented Devise for the user part. Thanks in advance for the help!
You need to create #user before using it. You can use factory_girl gem or directly do
#user = User.create
post :create, user_id: #user.id, item: { name: "name"}
Hope this works

Rails 4 StrongAttributes in RailsCast #274

I am new to Rails. Particularly in dealing with the vagaries between Rails 3 and 4. I have been learning from RailsCast and MHartl's tutorial.
I successfully got the code in RailsCast #274 to work by using the answer in the question linked below:
ActiveModel::ForbiddenAttributesError in PasswordResetsController#update
My concern is that this fix will leave me vulnerable to issues in the future, be it security or otherwise. If there is a "right" way to do this I would like to know. Here is my code block:
class PasswordResetsController < ApplicationController
def create
user = User.find_by_email(params[:email])
user.send_password_reset if user
redirect_to root_url, :notice => "Email sent with password reset instructions."
end
def edit
#user = User.find_by_password_reset_token!(params[:id])
end
def update
#user = User.find_by_password_reset_token!(params[:id])
if #user.password_reset_sent_at < 2.hours.ago
redirect_to new_password_reset_path, :alert => "Password reset has expired."
elsif #user.update_attributes(params.require(:user).permit(:password, :password_confirmation))
redirect_to root_url, :notice => "Password has been reset."
else
render :edit
end
end
end
you need to setup your params first. define a private method inside your class
private
def model_params
params.require(:model).permit(:list :all :your :attributes)
end
then when you do an update, use something like:
#model.update(model_params)
mass assignment is a cool thing in rails, but you need to make sure you are protected
hope that helps

Controller specs throwing up undefined method

I'm really struggling with Rspec DSL! I read a lot on SO and across the internet, so I'm posting my particular issue because my feeble mind can't get to the solution.
Have a post method in my controller that updates a user's email. Works fine, but I'm struggling with the spec because all I'm getting are undefined methods for NilClass (even though I've tried stubbing every object and method, etc).
users_controller.rb
def update_user_email
#user = User.find_by_id(params[:id])
new_email = params[:user][:new_email].downcase.strip
user_check = User.find_by_email('new_email')
if user_check.blank?
#user.email = new_email
#user.save
flash[:notice] = "Email updated to #{new_email}"
else
flash[:alert] = "This email is already being used by someone else!"
end
respond_with #user do |format|
format.html { redirect_to admin_user_path(#user) }
format.json { head :no_content }
end
end
Here's the spec I'm trying to write. What test should I be writing, if not this, and what can I do to prevent undefined method on NilClass errors!
users_controller_spec.rb
describe Admin::UsersController do
let!(:user) { FactoryGirl.create(:user, password: 'oldpass', email: 'bar#foo.com') }
...
describe "admin actions for each user" do
it "resets user email" do
post :update_user_email, {user: {new_email: 'foo#bar.com'} }
response.status.should == 200
end
end
...
end
And the error:
Admin::UsersController admin actions for each user resets user email
Failure/Error: post :update_user_email, {user: {new_email: 'foo#bar.com'} }
NoMethodError:
undefined method `email=' for nil:NilClass
The line that is failing is:
#user = User.find_by_id(params[:id)
Since you are not passing the id in during your test the user is not being found, and therefore you are trying to call email= on nil. Here is how you can clean up your controller and test.
class YourController < ApplicationController
before_filter :find_user, only: [:update_user_email]
def update_user_email
new_email = params[:user][:new_email].downcase.strip
user_check = User.where(email: new_email)
if user_check.blank?
#user.email = new_email
#user.save
flash[:notice] = "Email updated to #{new_email}"
else
flash[:alert] = "This email is already being used by someone else!"
end
respond_with #user do |format|
format.html { redirect_to admin_user_path(#user) }
format.json { head :no_content }
end
end
def find_user
#user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound
flash[:error] = "It looks like that user does not exist"
# redirect or render
end
end
# your test
describe "admin actions for each user" do
it "resets user email" do
post :update_user_email, id: user.id, user: {new_email: 'foo#bar.com'}
response.status.should == 200
end
end
You may also want to consider moving the logic out of the controller and into a service object. That controller method is getting a little long.
The issue is that you also need to pass the id of the User you're wanting to update. The line which is failing is #user.email = new_email, since #user is nil.
To get your test to pass now, you'll need to change your post method to:
post :update_user_email, {id:'bar#foo.com', user: {new_email: 'foo#bar.com'} }
As an aside, it is possible to say that it may be better for you to actually be doing this in the UsersController#update method, in order to maintain RESTful routes. And as for enforcing unique email address - it might be better to do this in the User class with validations.
In your post :update_user_email you're not passing :id... so #user = User.find_by_id... is not finding a user so #user is a nil object.
post :update_user_email, id: user.id, {user: {new_email: 'foo#bar.com'} }

How to stub a method on an already stubbed controller method with RSpec?

When a user im trying to unfollow doesn't exist I throw an exception that sets a flash error message and redirects the user back to the page they were on.
All the access to the twitter gem is handled by the TwitterManager class which is a regular ruby class that extend ActiveModel::Naming to enable the mock_model.
Now Im having a real hard time figuring out how this should be tested. The code below works but feels very wrong. The only way I could stub the twitter.unfollow method was with controller.send(:twitter).stub(:unfollow).and_raise(Twitter::Error::NotFound.new("", {}))
I tried using TwitterManager.any_instance.stub(:unfollow) but that did obviously not do what I thought it would do.
How can I make this better? What things have I totally misunderstood?
Spec
describe TwitterController do
before(:each) do
controller.stub(:twitter).and_return(mock_model("TwitterManager", unfollow: true, follow: true))
end
it "unfollows a user when given a nickname" do
#request.env['HTTP_REFERER'] = '/followers'
post 'unfollow', id: "existing_user"
response.should redirect_to followers_path
end
describe "POST 'unfollow'" do
it "does not unfollow a user that does not exist" do
controller.send(:twitter).stub(:unfollow).and_raise(Twitter::Error::NotFound.new("", {}))
#request.env['HTTP_REFERER'] = '/followers'
post 'unfollow', id: "non_existing_user"
flash[:error].should_not be_nil
flash[:error].should have_content("not found, could not unfollow")
response.should redirect_to followers_path
end
end
Controller
def unfollow
begin
twitter.unfollow(params[:id])
respond_to do |format|
format.html { redirect_to :back, notice: "Stopped following #{params[:id]}" }
end
rescue Twitter::Error::NotFound
redirect_to :back, :flash => { error: "User #{params[:id]} not found, could not unfollow user" }
end
end
[ more code ]
private
def twitter
twitter_service ||= TwitterFollower.new(current_user)
end
Rspec 2.8.0
Rails 3.2.0
You could clean it up a little by saving the mock TwitterManager as an instance variable in the before block and stubbing directly on that object:
describe TwitterController do
before(:each) do
#twitter = mock_model("TwitterManager", unfollow: true, follow: true)
controller.stub(:twitter).and_return(#twitter)
end
# ...
describe "POST 'unfollow'" do
it "does not unfollow a user that does not exist" do
#twitter.stub(:unfollow).and_raise(Twitter::Error::NotFound.new("", {}))
# ...
end
end
end
But I wouldn't say what you're doing is "very wrong" :-)

Resources