I have a user controller that when a PATCH request is sent to, it will try to authenticate the user and update the user's password. Everything works in the browser (I get 200 when the "current_password" is correct, and 401 when it's incorrect).
But when I try to run the specs, expect(response.status).to eq(401) is always failing because I always get 200 as the response, can anyone explain what's going on here?
users_controller.rb:
def update
if params[:user].has_key?("current_password") && !(#user.authenticate(params[:user][:current_password]))
render_response(:not_authorized, { description_detailed: "Unable to authenticate current password" })
else
#user.update_attributes!(accessible_params)
render_response(:ok, { data: UserSerializer.new(#user, root: false)})
end
end
users_controller_spec.rb:
context "when current password field is passed in" do
let(:name) { Faker::HarryPotter.character.split(' ') }
let!(:custom_user) do
create(:user,
first_name: name[0],
last_name: name[0],
email: "test#reset.com",
password: "correct123"
)
end
context "when current password is correct" do
before :example do
patch :update, params: { id: custom_user.id, user: user_params }
end
let(:user_params) do
{ current_password: "correct123", password: "newpassword123" }
end
it "updates user's password with new password" do
expect(custom_user.authenticate('correct123')).to eq(custom_user)
expect(response.status).to eq(200)
end
end
context "when current password is incorrect" do
let(:user_params) do
{ user: {
current_password: "incorrect1235",
password: "newpassword123"
}}
end
before :example do
patch :update, params: { id: custom_user.id, user: user_params }
end
it "does not update user's password" do
expect(custom_user.authenticate('incorrect1234')).not_to eq(custom_user)
expect(response.status).to eq(401)
end
end
end
This is because you are testing for the hash keys in string form in the controller...
if params[:user].has_key?("current_password")
but passing them in symbol form in the test...
user: {
current_password: "incorrect1235",
If you look at the parameters in controller in this case, you will find the key is:
:current_password
This also points out a dangerous flaw in your code -- a user's password can be updated if "current_password" is not supplied in the update parameters. In this instance the else block is always called and the user is never authenticated.
Suggest testing for the presence the current_password and returning an error if it is not supplied.
Related
In my controller, I have a method called update_password, this method updates the user password after validating token. But, my tests are failing.What might be the problem?
In my controller,
def update_password
user = User.find_by(email: params[:email])
if user.nil?
render json: { error: 'Could not update' }, status: 422
else
user.update(user_params)
render json: { message: 'Successfully Updated' }, status: 200
end
end
def user_params
params.permit(:password, :password_confirmation, :current_password, :email)
end
Test:
describe 'update password for valid token' do
it'should update the password' do
user_params = {
password: 'newpassword',
password_confirmation: 'newpassword',
email: user.email
}
put '/api/v1/update_password', params: user_params
expect(user.password).to eq 'newpassword'
expect(user.reload.password_confirmation).to eq 'newpassword'
expect(user.reload.password).to eq(user.reload.password_confirmation)
json_response = JSON.parse(response.body)
expect(json_response['message']).to eq('Successfully Updated')
end
end
Factories:
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}#example.com" }
password 'testcase'
username 'testcase'
password_confirmation 'testcase'
first_name 'testname'
last_name 'test'
end
end
Error I have got:
1) UsersRequests update password for valid token should update the password
Failure/Error: expect(user.password).to eq 'newpassword'
expected: "newpassword"
got: "testcase"
(compared using ==)
# ./spec/requests/users_requests_spec.rb:105:in `block (3 levels) in <top (required)>'
Finished in 0.35031 seconds (files took 5.69 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/requests/users_requests_spec.rb:98 # UsersRequests update password for valid token should update the password
Your controller action is fundamentally broken. It returns the wrong response code when a record is not found (422 not 401) and returns 200 no matter if the record is updated or not. You are also letting the user update the email attribute!
It should look something like this:
def update_password
# this will raise ActiveRecord::NotFound if the record cannot be found
# this avoids duplication by relying on rescue_from to return 401 - not found
user = User.find_by!(email: params[:email])
# You need to check the return value to see if the record was updated!
if user.update(update_password_params)
# consider omitting the JSON payload as clients can just
# use the status code to determine if it was a success
render json: { message: 'Successfully Updated' }, status: 200
else
render json: { error: 'Could not update' }, status: 422
end
end
private
# this should be a subset of the params and not allow email!
def update_password_params
params.permit(:password, :password_confirmation, :current_password)
end
You can also do much better in the spec by using RSpec's change matchers:
describe 'update password for valid token' do
let!(:user) { create(:user) }
let(:user_params) do
{
password: 'newpassword',
password_confirmation: 'newpassword',
email: user.email
}
end
# don't start with 'should'
it 'updates the password' do
expect do
put '/api/v1/update_password', params: user_params
user.reload
end.to change(user, :password).to('newpassword')
end
it 'is successful' do
put '/api/v1/update_password', params: user_params
expect(response).to have_http_status 200
end
end
Your spec should simply test the intendended behavior - that the records password is updated.
Testing the password_confirmation is not possible since its a virtual attribute - and not needed here. You instead need to test in a separate spec that the password is not updated if they do not match:
describe 'update password with invalid attributes' do
let!(:user) { create(:user) }
let(:user_params) do
{
password: 'newpassword',
password_confirmation: 'newpasswordxx',
email: user.email
}
end
it 'does not update the password' do
expect do
put '/api/v1/update_password', params: user_params
user.reload
end.to_not change(user, :password)
end
it 'reponds with 422' do
put '/api/v1/update_password', params: user_params
expect(response).to have_http_status 422
end
end
I am testing a controller method for creating new orders (e-commerce-like app). If user is present in the system, he should be redirected to new_user_session_path, else to new_order_path. Simple as that.
This is my orders_controller.rb
def new
if !User.where(phone: params[:phone]).blank? && !user_signed_in?
redirect_to new_user_session_path()
flash[:info] = "Already present"
else
#order = Order.new
#menu = Menu.find(params[:menu_id])
#menu_price = #menu.calculate_price(#menu, params)
end
end
In my app, I need the calculate_price method to be called, because it calculates the overall price given the params. But in my test, I just want to ensure, that the redirect is correct.
Right now I'm getting errors like (they are sourced inside the Menu.rb file, since calculate_price is called) :
Front::OrdersController#new redirects user to new order page if user is not present in the system
Failure/Error: menu_price_change = menu_amount.split(",")[1].gsub(" ","").gsub("]",'')
NoMethodError:
undefined method `split' for nil:NilClass
This is my spec file:
require 'rails_helper'
describe Front::OrdersController, type: :controller do
describe '#new' do
# Set up dummy menu
let (:menu) { Menu.create() }
it "redirects user to sign up page if user is present in the system" do
user = User.create(name: "Bob", password: "bobspassword", phone: "+7 (903) 227-8874")
get :new, params: { phone: user.phone }
expect(response).to redirect_to(new_user_session_path(phone: user.phone))
end
it "redirects user to new order page if user is not present in the system" do
non_present_phone = "+7 (903) 227-8874"
get :new, params: { phone: non_present_phone, menu_id: menu.id}
expect(response).to redirect_to(new_order_path)
end
end
end
Of course I could provide all the params, but there is a pretty big amount of them and besides, I just want to test the correct redirect. As far as I know, mocks and subs are useful in this case, when you want to explicitly test the methods. But in my case, I want to - somehow - omit them. How can I ensure that behaviour?
So you want just to test redirects and the errors occured when calculate_price method executes bother you. Why don't you just stub that method? Your spec file might be like this:
require 'rails_helper'
describe Front::OrdersController, type: :controller do
describe '#new' do
# Set up dummy menu
let (:menu) { Menu.create() }
# Check this out
before do
allow_any_instance_of(Menu).to receive(:calculate_price)
# or if you need certain value
allow_any_instance_of(Menu).to receive(:calculate_price).and_return(your_value)
end
it "redirects user to sign up page if user is present in the system" do
user = User.create(name: "Bob", password: "bobspassword", phone: "+7 (903) 227-8874")
get :new, params: { phone: user.phone }
expect(response).to redirect_to(new_user_session_path(phone: user.phone))
end
it "redirects user to new order page if user is not present in the system" do
non_present_phone = "+7 (903) 227-8874"
get :new, params: { phone: non_present_phone, menu_id: menu.id}
expect(response).to redirect_to(new_order_path)
end
end
end
I am trying to write a test for my InvitationsController#Create.
This is a POST http action.
Basically what should happen is, once the post#create is first executed, the first thing that needs to do is we need to check to see if a User exists in the system for the email passed in via params[:email] on the Post request.
I am having a hard time wrapping my head around how I do this.
I will refactor later, but first I want to get the test functionality working.
This is what I have:
describe 'POST #create' do
context 'when invited user IS an existing user' do
before :each do
#users = [
attributes_for(:user),
attributes_for(:user),
attributes_for(:user)
]
end
it 'correctly finds User record of invited user' do
post :create, { email: #users.first[:email] }
expect(response).to include(#users.first[:email])
end
end
end
This is the error I get:
1) Users::InvitationsController POST #create when invited user IS an existing user correctly finds User record of invited user
Failure/Error: post :create, { email: #users.first[:email] }
NoMethodError:
undefined method `name' for nil:NilClass
##myapp/gems/devise-3.2.4/app/controllers/devise_controller.rb:22:in 'resource_name'
# #myapp/gems/devise_invitable-1.3.6/lib/devise_invitable/controllers/helpers.rb:18:in 'authenticate_inviter!'
# #myapp/gems/devise_invitable-1.3.6/app/controllers/devise/invitations_controller.rb:67:in 'current_inviter'
# #myapp/gems/devise_invitable-1.3.6/app/controllers/devise/invitations_controller.rb:71:in 'has_invitations_left?'
I am using FactoryGirl and it works perfectly, in the sense that it returns valid data for all the data-types. The issue here is how do I get RSpec to actually test for the functionality I need.
Edit 1
Added my :user factory:
FactoryGirl.define do
factory :user do
association :family_tree
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
email { Faker::Internet.email }
password "password123"
password_confirmation "password123"
bio { Faker::Lorem.paragraph }
invitation_relation { Faker::Lorem.word }
# required if the Devise Confirmable module is used
confirmed_at Time.now
gender 1
end
end
It seems you're using Devise which require you to be logged in before going to the next step. On your error, Devise cannot get the same of your inviter because he's not logged.
Your test should be like this:
describe 'POST #create' do
context 'when invited user IS an existing user' do
before :each do
#users = [
attributes_for(:user),
attributes_for(:user),
attributes_for(:user)
]
#another_user = FactoryGirl.create(:user_for_login)
sign_in #another_user
end
it 'correctly finds User record of invited user' do
post :create, { email: #users.first[:email] }
expect(response).to include(#users.first[:email])
end
end
end
Example for FactoryGirl model for Devise
factory :user_for_login, class: User do |u|
u.email 'admin#myawesomeapp.com'
u.password 'password'
u.password_confirmation 'password'
u.name "MyName"
end
Of course, you need to add as much data as your validators want.. Basically for Devise you need email, password and password_confirmation. In you case, it seems you also need name.
The SessionController spec for an app I'm working on currently looks like this:
require 'rails_helper'
RSpec.describe SessionController do
let(:user) { create(:user, phone_verified: true, email_verified: true) }
describe "POST #create" do
context "with valid username and password" do
before do
post :create, user: { username: user.username, password: user.password }
end
specify { expect(response).to redirect_to(dashboard_path) }
specify { expect(session[:user_id]).to eq(user.id) }
end
context "with invalid username" do
before do
post :create, user: { username: "doesntexist", password: user.password }
end
specify { expect(response).to render_template(“login”) }
specify { expect(session[:user_id]).to be_nil }
end
context "with invalid password" do
before do
post :create, user: { username: user.username, password: "badpassword" }
end
specify { expect(response).to render_template(“login”) }
specify { expect(session[:user_id]).to be_nil }
end
end
end
The first two context blocks work exactly as expected. The examples in the third context block fail:
2) SessionController POST #create with invalid password
Failure/Error: post :create, user: { username: user.username, password: "badpassword" }
BCrypt::Errors::InvalidHash:
invalid hash
From what I've been able to find out on StackOverflow and by looking at the bcrypt-ruby source code, this means the password digest stored in the database is invalid. Since all three blocks are using the same let block to create the user, I'm at a loss as to why this would happen with an incorrect password and not a correct one. It also works normally when I start up rails s and try to log in through the view. Does anyone have any idea what is happening?
We're using the built-in has_secure_password method, and we're not using devise or any other authentication-related gems.
Resetting the database fixed this problem, although it's still not clear to me what caused it or why it only happened under such specific circumstances.
I am using Devise and JWT's to authenticate users in a project I am writing. I am having a hard time figuring out how to write a useful test to expect a JWT response.body (since each is encrypted).
The only thing I can think of is to test that they are structured as a JWT should be (a 3 segment, '.' delimited string).
Has anyone encountered testing random/hashed returns and come up with a better solution?
describe SessionTokensController, type: :controller do
let(:current_user) { FactoryGirl.create(:user) }
before(:each) do
sign_in current_user
end
describe '#create' do
it 'responds with a JWT' do
post :create
token = JSON.parse(response.body)['token']
expect(token).to be_kind_of(String)
segments = token.split('.')
expect(segments.size).to eql(3)
end
end
end
It really depends on what exactly you want to test.
If you simply want to test if the returned token exists and is valid you can do the following:
it 'responds with a valid JWT' do
post :create
token = JSON.parse(response.body)['token']
expect { JWT.decode(token, key) }.to_not raise_error(JWT::DecodeError)
end
Although it seems much more useful to validate the claims that the token includes:
let(:claims) { JWT.decode(JSON.parse(response.body)['token'], key) }
it 'returns a JWT with valid claims' do
post :create
expect(claims['user_id']).to eq(123)
end
In the latter example you can validate the exact claims you included in the JWT.
let(:user) { create(:user, password: "123456") }
describe "POST authenticate_user" do
context "with a valid password" do
it "authenticates successfully" do
post :authenticate_user, params:{email: user.email, password: "123456"}, format: :json
parsed_body = JSON.parse(response.body)
# binding.pry
expect(parsed_body.keys).to match_array(["auth_token", "user"])
expect(parsed_body['user']['email']).to eql("joe#gmail.com")
expect(parsed_body['user']['id']).to eql(user.id)
end
it "authentication fails" do
post :authenticate_user, params:{email: user.email, password: "123456789"}, format: :json
parsed_body = JSON.parse(response.body)
expect(parsed_body['errors'][0]).to eql("Invalid Username/Password")
end
end
end