Devise + RSpec : How to authenticate a user? - ruby-on-rails

I am using Devise for member authentication.
I need to test the log in scenario for a member. sign_in(member) doesn't check authentication information - it just signs in the member thats why i am using authenticate_member! method for authentication as suggested in Github Issue: How to authenticate the user with warden/devise in a customized way? .
BUT I get an exception saying ArgumentError: uncaught throw :warden.
#spec/features/member.rb
FactoryGirl.define do
factory :member do
email "john#gmail.com"
password "12345678"
firstname "John"
lastname "Doe"
location "United States"
end
end
#spec/controllers/sessions_spec.rb
it "authenticate member" do
create(:member)
#request.env["devise.mapping"] = Devise.mappings[:member]
#request.env["warden"] = warden
controller.allow_params_authentication!
expect(controller.authenticate_member!(:scope => :member, :force => true)).not_to be nil
end
I also tried to set controller.resource = FactoryGirl.attributes_for(:member) thinking that resource is not set but then i get exception as
NoMethodError: protected method 'resource=' called for #<Devise::SessionsController:0x007ffc4e5bab10>
How to resolve this issue?

I posted a github issue on Devise regarding this question: Github Issue: RSpec with Devise : How to authenticate a user?
I was advised to use a post to sessions#create in order to simulate the POST request from the sign in form.
Here is the alternative solution:
#spec/controllers/sessions_spec.rb
it "authenticate member" do
create(:member)
#request.env["devise.mapping"] = Devise.mappings[:member]
#request.env["warden"] = warden
## Added post request to "create"
post :create, :member => FactoryGirl.attributes_for(:member)
## Added new expectations
expect(controller.member_signed_in?).to be true
expect(controller.current_member.email).to eq "john#gmail.com"
## "session["warden.user.member.key"][0].first" stores the "id" of logged in member
## Replace "member" in "session["warden.user.member.key"][0].first" with your
## devise model name. For example: If your devise model name is customer then you
## need to check "session["warden.user.customer.key"][0].first"
expect(session["warden.user.member.key"][0].first).to eq member.id
end
NOTE:
I would have liked to get an explanation of why my previous code as suggested in question and as provided by Devise team in Github Issue: How to authenticate the user with warden/devise in a customized way? didn't work out which it should have. Feel free to post a new answer if anyone finds an explanation as to why my previous code for member authentication didn't work.

Related

Rails 4 with Devise, Omniauth and multiple service providers

I am trying desperately to find a way to get devise to work in my Rails 4 app. I have tried every tutorial I can find to get this set up.
The current tutorial is:http://sourcey.com/rails-4-omniauth-using-devise-with-twitter-facebook-and-linkedin/
My previous bounty questions on related problems (commonly voted down & I don't understand why), show my other attempts at getting this done.
My problem currently is with this bit of text in the user model:
def self.find_for_oauth(auth, signed_in_resource = nil)
# Get the identity and user if they exist
identity = Identity.find_for_oauth(auth)
# If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date.
user = signed_in_resource ? signed_in_resource : identity.user
# Create the user if needed
if user.nil?
# Get the existing user by email if the provider gives us a verified email.
# If no verified email was provided we assign a temporary email and ask the
# user to verify it on the next step via UsersController.finish_signup
email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
email = auth.info.email if email_is_verified
user = User.where(:email => email).first if email
# Create the user if it's a new registration
if user.nil?
user = User.new(
name: auth.extra.raw_info.name,
#username: auth.info.nickname || auth.uid,
email: email ? email : "#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0,20]
)
user.skip_confirmation!
user.save!
end
end
The problem is (pointing at this line: user = User.new) in the above method:
unknown attribute 'name' for User.
In the previous tutorial I tried, I discovered that linkedin worked when i changed the attributes to:
# self.email = omniauth['extra']['raw_info']['emailAddress']
# self.first_name = omniauth['extra']['raw_info']['firstName']
# self.last_name = omniauth['extra']['raw_info']['lastName']
The field names for the omniauth strategy are different to what's shown in the tutorial (at least for linkedin). But then if Facebook or twitter use different field names again, how does the tutorial solve for that?
Also, in my user table, I have attributes called first_name and last_name, but I tried changing the line to:
first_name: auth.extra.raw_info.'firstName',
That didn't work either.
When I try:
first_name: auth.extra.raw_info.name
I get this error:
undefined method `password_required?' for #<User:0x007fb7bc2ce6f8>
But anyway, I only want first name in that field and i think this is putting the whole name into first name (although Im not sure about that). Also, if this is going to be amended to work for linkedin, will that mean it will not work for Facebook and twitter?
It's all a big mess. I'm growing increasingly frustrated with this. Does anyone know how to solve this particular problem. I have been trying for 2.5 years to get devise/omniauth working.
My recent previous tutorial linked questions are:
https://stackoverflow.com/questions/33888972/rails-devise-omniauth-strategies-omniauthcallbackscontroller
Devise Omniauth - setup & defining strategies
Rails, Devise & Omniauth - problems with setup
There are several others, but I'm not figuring this out by my own efforts. I've had a few sessions on codementor.io but not been able to find help. A source of help would be greatly appreciated.
So trying the suggestion below, I tried changing the method in the user model to:
def self.find_for_oauth(auth, signed_in_resource = nil)
# Get the identity and user if they exist
identity = Identity.find_for_oauth(auth)
# If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date.
user = signed_in_resource ? signed_in_resource : identity.user
# Create the user if needed
if user.nil?
# Get the existing user by email if the provider gives us a verified email.
# If no verified email was provided we assign a temporary email and ask the
# user to verify it on the next step via UsersController.finish_signup
email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
email = auth.info.email if email_is_verified
user = User.where(:email => email).first if email
# Create the user if it's a new registration
if user.nil?
user = User.new(
case auth.provider
when 'linkedin'
first_name: auth.extra.raw_info.firstName,
last_name: auth.extra.raw_info.lastName,
email: emailAddress ? email : "#{auth.uid}-#{auth.provider}.com",
#username: auth.info.nickname || auth.uid,
# email: email ? email : "#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0,20]
when 'facebook'
first_name: auth.extra.raw_info.first_name,
last_name: auth.extra.raw_info.last_name,
email: auth.extra.raw_info.email ? email : "#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0,20]
when 'twitter'
first_name: auth.extra.raw_info.nickname,
end
)
user.skip_confirmation!
user.save!
end
end
There are several problems with this, being:
unexpected ':', expecting keyword_end first_name: auth.extra.raw_info.firstName,
unexpected tLABEL, expecting '=' last_name: auth.extra.raw_info.lastName
unexpected tLABEL, expecting '=' email: emailAddress ? email : "#{au... ^
unexpected ',', expecting keyword_end
unexpected keyword_when, expecting keyword_end when 'facebook' ^
unexpected ':', expecting keyword_end first_name: auth.extra.raw_info.first_name, ^
unexpected tLABEL, expecting '=' last_name: auth.extra.raw_info.last_name,
unexpected tLABEL, expecting '=' email: auth.extra.raw_info.email ? ... ^
unexpected ',', expecting keyword_end
unexpected keyword_when, expecting keyword_end when 'twitter' ^
unexpected ':', expecting keyword_end first_name: auth.extra.raw_info.nickname, ^
unexpected keyword_end, expecting '='
I don't know how to solve any of this - I can't find any example (other than the one in the post below) of how to approach this - and that's clearly not working).
On top of all of the above, what do I do with Twitter? It has a nickname field. How can I separate the words out so the first word is saved as first_name and the 2nd word is saved as last_name?
Extrapolating from the particularly snarky comments below, I tried that suggestion again, with if statements. It still doesnt work.
def self.find_for_oauth(auth, signed_in_resource = nil)
# Get the identity and user if they exist
identity = Identity.find_for_oauth(auth)
# If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date.
user = signed_in_resource ? signed_in_resource : identity.user
# Create the user if needed
if user.nil?
# Get the existing user by email if the provider gives us a verified email.
# If no verified email was provided we assign a temporary email and ask the
# user to verify it on the next step via UsersController.finish_signup
email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
email = auth.info.email if email_is_verified
user = User.where(:email => email).first if email
# Create the user if it's a new registration
if user.nil?
user = User.new(
if auth.provider = linkedin
first_name: auth.extra.raw_info.firstName,
last_name: auth.extra.raw_info.lastName,
email: emailAddress ? email : "#{auth.uid}-#{auth.provider}.com",
#username: auth.info.nickname || auth.uid,
# email: email ? email : "#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0,20]
end
if auth.provider = facebook
first_name: auth.extra.raw_info.first_name,
last_name: auth.extra.raw_info.last_name,
email: auth.extra.raw_info.email ? email : "#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0,20]
end
if auth.provider = twitter
first_name: auth.extra.raw_info.firstName,
end
)
user.skip_confirmation!
user.save!
end
end
Absent finding a solution to this on this board, I would appreciate advice on how much you think it would be reasonable to pay a professional to help resolve these problems.
I HAVE GONE RIGHT BACK AND COMMENTED OUT ALL OF THE CODE RELATED TO DEVISE AND OMNIAUTH AND NOW TRYING AGAIN, WITH THE DOC ON THE OMNIAUTH WIKI CALLED: MANAGING MULTIPLE PROVIDERS
It seems this doc may have typos in it that experienced coders can read past.
Currently, there is an error message being generated as follows:
/Users/config/routes.rb:35: syntax error, unexpected [, expecting keyword_do or '{' or '(' ...', to: 'sessions#create', via [:get, :post] ... ^ /Users/config/routes.rb:36: syntax error, unexpected [, expecting keyword_do or '{' or '(' match '/logout', to: 'sessions#destroy', via [:get, :post] ^
I have copied these routes directly from the user guide. I'm by far from an expert but I'm also confused about why '==' is used in some places and '=' is used in others in this doc. For example:
if signed_in?
if #identity.user == current_user
Whilst:
#identity.user = current_user
In the same method there is a variance.
I'm also confused about why the sessions controller doesnt inherit from the devise controller. In each of the other tutorials I have done that have had a sessions controller, it has inherited from devise.
There are quite a few other confusing aspects of this tutorial (like why doesnt it have a registrations controller, why are there no other routes for devise, why does the application controller have create and destroy methods)?
Desperately seeking help.
This is not a complete answer, but I have solved part of my problem.
The point of the oauth gem is to take the attributes from each social media strategy and unify them into a common form of expression.
By incorporating raw_info from the strategy, the code is not working with oauth. For example, linkedin auth hash returns data labelled 'firstName' where oauth recognises that as first_name. If you use
first_name: auth.extra.raw_info.first_name,
the attribute will be nil when linked is being called. This is already strange to me since LinkedIn gives basic profile details which suggest a label is first-name.
Anyway, the part I fixed is to remove reference to 'extra.raw' in the above line and use auth.info.first_name. That should resolve differences between strategy labels.
I am still working on the rest of the problems arising in this set up. Some of the issues in the sourcey tutorial are syntax, others are more substantial. I'll post again if I can sort them out.
unknown attribute 'name' for User.
This means that there's no name column in users table. You need to create this column or use some other column that you have (like first_name or username). Like so:
username: auth.extra.raw_info.name
But then if Facebook or twitter use different field names again, how
does the tutorial solve for that?
Yeah, this tutorial gives only one variant, but others are not so difficult to find out. You just need to know, what fields returns this or that provider (twitter, facebook or linkedin) and do something like this:
def self.find_for_oauth(auth, signed_in_resource = nil)
...
# Create the user if needed
if user.nil?
case auth.provider
when 'facebook'
# Take fields that Facebook provides and use them when creating a new user
when 'twitter'
# Take fields that Twitter provides and use them when creating a new user
when 'linkedin'
# Take fields that Linkedin provides and use them when creating a new user
end
end
...
end
It will be better for code organization if you implement separate methods for each provider and call them in certain when.
Also check this out https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema to understand what's inside env['omniauth.auth'].

How do I access a virtual attribute on a FactoryGirl-generated user in an Rspec/Capybara test?

In teaching myself Ruby and Rails, I'm currently building a user system based closely on the Hartl tutorial; the main difference at this point is that I'm testing it with Rspec/Capybara rather than TestUnit. So far most things have been fairly straightforward, but I've been stuck on this one problem in testing password resets.
When I test password resets manually, they work exactly as expected. Hence I'm pretty sure the problem is somewhere between Rspec, Capybara, and FactoryGirl.
The spec code that keeps failing is when I want to visit the password forgotten link:
# password_resets_spec.rb
visit(edit_password_reset_path(spec_user.reset_token, email: spec_user.email))
It gives me:
ActionController::UrlGenerationError:
No route matches {:action=>"edit", :controller=>"password_resets", :email=>"john2#example.com",
:format=>nil, :id=>nil} missing required keys: [:id]
Password resets are a (partial) resource, and their id is the user's reset_token.
spec_user is generated by FactoryGirl:
#password_resets_spec.rb
let(:spec_user){ FactoryGirl.create :user }
The relevant :user factory doesn't set reset_token but when I try it with one that does, the token does not get properly set by the reset form, and doesn't match the generated digest. Here is the code that sets both:
# user.rb
def create_reset_digest
self.reset_token = User.new_token
update_attribute(:reset_digest, User.digest(reset_token))
update_attribute(:reset_sent_at, Time.zone.now)
end
I can bypass the above error by using a factory that does set reset_token, but then the token used for the route doesn't match the digest saved, and the next step in the test fails.
My conclusion so far is that I'm misunderstanding something about how virtual attributes are handled, as the token is virtual while the digest is saved to database. I suspect that the reset_token I get for my route in the spec is not the same one involved in generating the digest, somehow.
Any help understanding what's going here would be much appreciated! :)
You are getting a route error because there is no value for the reset_token; your id in this case.
You can set / assign virtual attributes on factory created objects. Just pass the value to the factory:
let(:spec_user){ FactoryGirl.create :user, reset_token: "some value"}
However, for your use case, I'm guessing that you also need the hashed value to be in the database. pasword_resets#edit probably does a lookup of the user record by hashing the incoming reset_token.
So, in your spec:
require 'spec_helper'
describe "PasswordResets" do
describe "#edit" do
it "looks up the user and provides a form to reset the password" do
# Generate the token in advance, so you can set the digest on the user
reset_token = User.new_token
# Create your user with the digest
spec_user = FactoryGirl.create(:user, reset_token: reset_token, reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
# Now visit path with an actual value for reset_tokeb
visit(edit_password_reset_path(spec_user.reset_token, email: spec_user.email))
end
end
end

rspec, request spec, devise, multi-controller

I'm trying to build a request spec which tests the full gamut of creating a new user via devise API.
I currently have this sitting in RegistrationsController spec, but this won't work if I want to follow the mail link to the confirmations controller.
I haven't been able to find a good example of how people have tested the 'hand-off' from one controller to another and the intermittent 'steps' (we have custom devise methods scattered throughout the process which this test will encompass too).
it "creates a user, sends a welcome email, confirms the user, and registers the user in email campaigns" do
post :create, {user: new_user_params}
last_email = ActionMailer::Base.deliveries.last.body
ConfirmationsController.any_instance.should_receive(:after_filter_method_to_subscribe_user)
redirect_to confirmation_link(last_email) # helper method
last_email.should include("Thanks for joining!")
user = User.find_by_first_name(new_first_name)
user.confirmed?.should be_true
user.email_lists.should_not be_empty
end
Edit:
I should also add that I need http_basic_auth to run the spec which I'm including in a spec/support file and sets the request.env['HTTP_AUTHORIZATION'] to variables stored in the API::Base controller. I currently have nil as a request obect when running specs in the spec/request folder, which I'll need to run the specs.
Edit:
Thanks to people who've taken a look. I figured it out after piecing together two SO searches and the code I had. I'll post an answer for future SO'ers when I can.
I figured this out shortly after posting my question with good luck finds on more google searches. Kudos to a couple SO references ~> request spec relish: http://goo.gl/iBg7v1 && setting request headers for http basic auth in request specs: http://goo.gl/hdDBMd
My spec turned out to look something like the below Hope this helps someone not waste 4 hours like me :).
spec/requests/api/user_registration_spec.rb.
it "sends a welcome email, confirms the user, and signs the user up to email campaigns" do
email_list = FactoryGirl.create(:email_list, name: "funky-email-campaign")
user_name = Api::RegistrationsController::USER
password = Api::RegistrationsController::PASSWORD
# post to /users/registration
post api_registrations_path({user: new_user_params}), nil , {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user_name, password)}
last_email = ActionMailer::Base.deliveries.last.body
UserService.should_receive(:subscribe_to_email).and_call_original # check that after_filter is called
get confirmation_link(last_email) # follow link in email (/users/confirmation)
response.should redirect_to(custom_path) # tests after_confirmation_path_for override
last_email.should include(new_first_name)
last_email.should include("Thanks for joining!")
user = User.find_by_first_name(new_first_name)
user.confirmed?.should be_true
user.email_lists.first.name.should eq(email_list.name)
end

Access the current user in Cucumber features - Devise

Note: I am very new to Cucumber.
I am trying to make a generalized step (not sure if one already exists somewhere or not) so that you can easily add objects to another object, given the association exists. I want to do something like:
manage_notes.feature
Background: Login User
Given the following user records
| email | password |
| test#email.com | password |
Given I am logged in as "test#email.com" with password "password"
Scenario: Edit existing note
Given I have already created a note that belongs to current_user
general_steps.rb
Given /^the following (.+) records?$/ do |factory, table|
table.hashes.each do |hash|
Factory(factory, hash)
end
end
Given /^I am logged in as "([^\"]*)" with password "([^\"]*)"$/ do |email, password|
unless email.blank?
visit new_user_session_path
fill_in "Email", :with => email
fill_in "Password", :with => password
click_button "Sign In"
end
end
note_steps.rb
Given /^I have already created a (.+) that belongs to (.+)$/ do |factory, resource|
model = Factory(factory)
resource.send(model.class.to_s.downcase.pluralize) << model
end
Seems like there might be a way to use the devise 'current_user' helper.
What is the correct way to accessing the user that is logged in?
Please let me know if you need more information.
Thanks!
UPDATE 1:
I have temporarily fixed my issue by creating a new step that allows me to do:
Given I have already created a note that is owned by the user with email "test#email.com"
But I don't want to specify the email, I'd still like to be able to use the logged in user if possible.
UPDATE 2:
Added general_steps.rb
So you can see, that in my 'Background', the user is created via a Factory, and then is logged in via my interface. I want to access the model of that logged in User.
I don't use Devise, so I can't answer specifically to if Devise has method of access the current_user.
But I actually like to use Pickle to help me keep my references. And perhaps this can help you out till you find a more Devise specific way to achieve what you want.
Given /^the following (.+) records$/ do |factory, table|
table.hashes.each do |hash|
Factory(factory, hash)
# since this is like an all encompassing factory creator, this line to
# create a Pickle references might need a bit more extra work if you want
# to create references for other factory types
# I assume email is unique, else use a unique identifier of some sort
find_model! %{user: "#{hash['email']}"}, {:email => hash['email']}
end
end
Given /^I have already created a (.+) that belongs to #{capture_model}$/ do |factory, name|
model = Factory(factory)
ref = model!(name) # we find that reference here
ref.send(model.class.to_s.downcase.pluralize) << model
end
This would read
Given I have already created a note that belongs to user: "test#email.com"
# I would just change this to
Given I have already created a note
# or
Given a note was created for user: "test#email.com"
You are I since you said Given I logged in..., no need to say that belongs to user: "test#email.com" it's already you.
Not to mention it could lead to confusion when you read it, some people may think you are adding a note to a user, who they might now know (or realize) is actually yourself.
While you still have to reference explicitly (eg. user: "John Doe"), I think that is a plus. By always calling specific references, everyone knows who is being referenced and there is no question about who is doing what to what.
Pickle serves us very well for this purpose. The only problematic areas we find are with things created directly through the app's ui, which gets a bit tricky to ensure you are creating the right reference to it.
Pickle has a large number of uses so definitely take a look.
Upate
You will have to find yourself here. Since, like you wanted, there is no current_user option (as far as we know). So you basically have to go find the "actor" in this scenario.
Given /^I have already created a note$/ do
user = model!(%{user: "test#email.com"}) # if using pickle
# user = User.find_by_email("test#email.com") # if just regular AR
user.notes << Note.create
end
I just solve it very simple:
I have "FactoryGirl" user defined, then this method...
Given(/^I am users logged in as "(.*?)" with password "(.*?)"$/) do |email, pass|
visit new_user_session_path
fill_in "user_email", :with => email
fill_in "user_password", :with => pass
click_button I18n.t("login.sign_in")
#current_user = User.find_by_email(email)
end
further on You could use #current_user in Your steps
This topic is quite old, but here is the solution I use. You must load this module, in env.rb, for example. Using it you can access the current user using either #current_user or current_user in your steps.
module UserSessionHelper
def current_user
#current_user
end
def login(user=nil)
if user.nil?
#current_user = FactoryGirl.create(:user, username: fake_name)
else
#current_user = user
end
visit login_path
fill_in 'Username', with: #current_user.username
fill_in 'Password', with: '1234'
click_button 'Login'
page.should have_content('Logout')
end
def logout
click_link 'Logout'
end
end
RSpec.configure do |config|
config.include UserSessionHelper
end if RSpec.respond_to?(:configure)
World(UserSessionHelper) if respond_to?(:World)
You should be able to use context for this:
http://cuke4ninja.com/sec_sharing_context.html
Have a step where you login as a user, store it in a shared context accessible to all steps and then just access it in following steps.
Hope this makes steps and I did not misinterpret the question.
Good luck with that!

rails Devise - How to redirect to a Getting Started Link - After validating an email

I'm using Devise with my rails 3 app. The app requires users to validate their email before continuing.
How can I redirect users to a specific url like /gettingstarted after they successfully validate their email address via the email confirmation msg they receive?
Thanks
When a user clicks on the confirm link they are taken to a confirm page which checks the confirmation token and if it's valid automatically logs them into the application. You could overwrite the after_sign_in_path_for method in your ApplicationController (as shown on the Devise wiki) and then redirect them to your getting started page the first time a user logs in.
def after_sign_in_path_for(resource_or_scope)
if resource_or_scope.is_a?(User) && first login
getting_started_path
else
super
end
end
For "first login" you could test if the confirmed_at timestamp is within a couple minutes of now, if your also using the trackable module in devise you can check if the sign_in_count is 1 or you could create your own field in the user model that tracks this information.
I'm checking devise source code at https://github.com/plataformatec/devise/blob/master/app/controllers/devise/confirmations_controller.rb
and seems that we have a callback to do it "after_confirmation_path_for"but I couldn't get it working without rewrite Devise::ConfirmationController
I hope that helps and if somebody get it working just defining after_confirmation_path_for just let us know.
I'm using the last_sign_in_at field from the 'trackable' model to achieve this. I've got the following code in my root action:
if current_user.last_sign_in_at.nil? then
redirect_to :controller => :users, :action => :welcome
end
http://rubydoc.info/github/plataformatec/devise/master/Devise/Models/Trackable
Seems to work reasonably well.
inside the 'after_sign_in_path_for' the current_user.last_sign_in_at.nil? will not work since it is alerady after the first sign-in. However this will work
if current_user.sign_in_count == 1
# do 1 thing
else
# do another thing
end

Resources