Devise + RSpec + Capybara : Test that new session created successfully when user logs in - devise

In my rails application i am using Devise for user authentication.
When a user logs in to the application, Devise authenticates that user and creates a session for that particular user.
I need to test that, when a user logs in, a session is created for that user. How do i do that?
In my acceptance test, If i do something like ,
feature 'User logs in' do
scenario 'with valid email and password' do
log_in_with 'valid#example.com', 'password'
expect(page).to have_content('Log out')
end
def log_in_with(email, password)
visit new_user_session_path
fill_in 'Email', with: email
fill_in 'Password', with: password
click_button 'Log in'
end
end
Still i am not checking that a new session was created for the logged in user. This does not fulfill my requirement as i am not checking if an actual user session was created.
Now, if i use warden instead:
user = FactoryGirl.create(:user)
login_as(user, :scope => :user)
...
i am just saying that login as so and so user and go to next steps like maybe check for a Log Out link or an Edit Profile link, but no check on session creation for the user.
If i do this in controller specs, where i have access to sign_in and sign_out methods :
describe Devise::SessionsController do
it "user signed in successfully" do
logged_user = User.create(:email => "valid#example.com", :password => "12345678")
sign_in logged_user
expect(controller.user_session).not_to be nil
end
end
Here, i have access to user_session, current_user, user_signed_in? methods.
Out of these 3 choices, i opted for user_session as it seems to be associated with current user session.
Is this the correct approach?
Or is there another alternative or direct method available to check that a session is active or was created for a user?
Is there a way to check this scenario in my acceptance test?

I'm pretty sure you can access all of your session data using the session hash in Controller specs. You could use it, let's say, to check if the user_id session key has been set after signing in:
expect(session[:user_id]).to_not be_nil
And, of course, you can use this to check any session that you might add throughout your application.
If you really want to see the session created by Devise, you can access the warden key generated in session:
expect(session["warden.user.user.key"][0].first).to eql logged_user.id
It's kind of unnecessary because this is the kind of thing that is already tested in Devise, so testing just if the user is logged in should be enough.
There's a great post about testing Omniauth controllers that is also a nice guide for testing SessionsController. It uses that old should syntax of RSpec, but it should be understandable =P

Related

omniauth-devise error: "Validation failed: Email has already been taken"

I am trying to create a rails 4 app with omniauth devise :
signin with facebook
signin with google
signin with linkedin
signin with twitter
Here, I am able to login in with either facebook, linkedin, twitter or google account. But my problem is: my google account email and linkedin email address are same. And login with google and then login with linkedin is giving me this error:
"Validation failed: Email has already been taken"
This is a problem because devise uses :unique => true in migration file for email field.
Can anyone provide me nice idea to handle this error please?
I think that basically, if handling all the cases properly, this can be really complex unless you choose option 1 below (and even then, there are issues to consider which I outline afterwards). Sorry for the length of this answer!
I'm assuming you've done something like this:
https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview
so far, which gets you some of the way but doesn't handle the problem you're encountering.
The way I have done this, I have a User who has_many Identities. Identity stores the name of the external service, the user id it tells you and whatever else you want. This means that the same user can log in with multiple identities (twitter, Facebook…). Have you seen:
http://railscasts.com/episodes/235-omniauth-part-1?view=asciicast
http://railscasts.com/episodes/236-omniauth-part-2?view=asciicast
which help with getting going with the User has_many Identities, but still don't deal with your case.
Then to solve your issue, one option is to detect the validation error you're encountering:
if #user.errors.added?(:email, :taken)
# do whatever you want - e.g. one of the 4 options below.
end
and if it occurs, you could either:
just add the identity to identities associated with that existing user who has the same email address and then sign them in.
or
before adding the identity to the existing user, ask for the password for the existing user account (if the account was originally registered via devise on your system), so you'll need to go to some new controller/action and view that handles this flow.
or
Perhaps send an email confirmation (not devise's standard confirmation) to acknowledge that they are linking their new identity to an existing account. This sounds a bit complicated, because you'll have to store the identity temporarily somewhere (probably in a database if you want to cope with them ending their current session before clicking a confirmation link), flagging it as unconfirmed, until they click a confirmation link in an email (which you'll also have to deal with generating).
or
Perhaps force them to authenticate with the other identity that has the same email address. This has the advantage over the previous option that you can just save the new identity info in the session and get them to authenticate using the other service immediately, but obviously there will be some work to handle the flow there.
Option 1 can be less secure because you are trusting that external services have confirmed the user's email address - which perhaps they have - but you want to be sure otherwise someone could sign up with linkedin using their email address and then sign in to your site and then an attacker could sign up with another service but using the same email address. They could then access the person's account on your site if you didn't confirm they really owned the email address somehow (e.g. by using option 2, 3 or 4). If the external services can confirm they've verified the email addresses, then this option should be ok and is by far the simplest - for example, Facebook have a field that tells you the account has been verified (but see my comments below about services that don't need email addresses). If they're merging with an account registered with you directly (doesn't sound like your situation), you should have confirmed the email address they registered using devise's standard Confirmable feature.
Option 2 sounds like it doesn't apply in your case, because you don't mention that a user can register with you directly via devise; only sign in using external services. This means that they have no password on your site that they know. (You've probably added a dummy one to get the devise validation to pass, unless you've disabled that, but they won't know the password unless you've told them somehow, and it could be confusing to them to do that).
Option 3 sounds do-able, though I haven't tried it. It's a bit more laborious for the user.
Option 4 also sounds do-able, though I haven't tried it either.
So far, I've done option 2 because users can only register with my site either directly using devise or via 1 external service. I'll be adding other services soon, so I plan to use option 4 (perhaps only if external service says they haven't confirmed the email address, and option 1 otherwise).
Options 2, 3 and 4 are a fair bit more work than option 1, so it depends if you can confirm that the external services have verified the email addresses and if not, how paranoid you are about attackers being able to access user accounts on your site. Personally, I err on the side of paranoia!
This might also give you some more useful info:
https://github.com/intridea/omniauth/wiki/Managing-Multiple-Providers
but because omniauth itself doesn't concern itself with model issues, it mostly sidesteps it, though it says for your case that it is "probably sufficiently prudent to assume that they are, in fact, the same person who also created the previous user" but you have to be able to trust the external services as I mentioned above.
There are also other things to consider, such as the case where someone has the same email address registered with Facebook and linked in and has signed in with both on your site (so single user account once you've dealt with your issue) and then changes the email associated with their Facebook account but not linkedin. If you always overwrite the email stored in the user table with the one from the external service, then it'll keep changing if they log in with linkedin and then Facebook (but maybe this doesn't matter to you). Alternatively, they may have different email addresses registered with Facebook and linked in and have logged in with both on your site (so 2 different users on your site) and then they change their linked in email address to be the same as the Facebook one. If you update the email address for a user every time they log in via an external service, you'll have your "Email already taken" error, but in this case you have 2 existing users to merge which could be interesting depending on what else in your database is associated with a user...
Also, I don't think twitter returns an email address, so if the same person has logged in with twitter and linkedin, you won't detect this. Furthermore, I think email is optional with Facebook (you can use a mobile phone number), so the same thing can happen with Facebook. My ideal solution would allow the user to merge arbitrary accounts, obviously having to enter whatever credentials are required to confirm they own the accounts they are merging! I haven't done this yet, but it's on my wish list!
I followed these steps and working fine for me:
1. Gemfile
gem 'omniauth-facebook', '1.4.0'
gem 'omniauth-twitter'
gem 'omniauth-google-oauth2'
2. config/route.rb
devise_for :users, controllers: { omniauth_callbacks: "omniauth_callbacks" }
3. Links
<%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>
<%= link_to "Sign in with twitter", user_omniauth_authorize_path(:twitter) %>
<%= link_to "Sign in with google", user_omniauth_authorize_path(:google_oauth2) %>
4. controllers/omniauth_callbacks_controller.rb
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_filter :authenticate_user!
def all
user = User.from_omniauth(request.env["omniauth.auth"], current_user)
if user.persisted?
flash[:notice] = "you are successfully logged in!!"
sign_in_and_redirect(user)
else
session["devise.user_attributes"] = user.attributes
redirect_to new_user_registration_url
end
end
def failure
super
end
alias_method :facebook, :all
alias_method :twitter, :all
alias_method :google_oauth2, :all
end
5. add required fields and model
rails g migration add_social_network_info_columns_to_users name image_url locations
# generate new model Authorization
rails g model Authorization user_id:integer provider uid token secret username
6. models/User.rb
class User < ActiveRecord::Base
require 'securerandom'
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:omniauthable
has_many :authorizations
# omniauth facebook provider
def self.from_omniauth(auth, current_user)
# check for existing authorization
# Find or create Authorization with: provider, uid, token and secret
authorization = Authorization.where(
:provider => auth.provider,
:uid => auth.uid.to_s,
:token => auth.credentials.token,
:secret => auth.credentials.secret
).first_or_initialize
if authorization.user.blank?
user = current_user.nil? ? User.where('email = ?', auth["info"]["email"]).first : current_user
# save user related data in user table
if user.blank?
User.new(
:email => auth.info.email,
:password => Devise.friendly_token[0,10],
:name => auth.info.name,
:locations => auth.info.location,
:image_url => auth.info.image
)
# since twitter don't provide email,
# so you need to skip validation for twitter.
auth.provider == "twitter" ? user.save!(:validate => false) : user.save!
end
# store authorization related data in authorization table
authorization.username = auth.info.nickname
authorization.user_id = user.id
authorization.save!
end
authorization.user
end
end
6. model/Authorization.rb
class Authorization < ActiveRecord::Base
belongs_to :user
end
source:
https://github.com/mohitjain/social-login-in-rails

I don't have offline access to fb user - should I store friends list in session? RoR, Devise, Omniauth & FBGraph

I authenticate a user on my app via facebook. While doing this, I obtain the access token and perform my validation. However, soon after signing in, I will have to present my user with their list of friends. Problem is that the access token has expired by then.
Given that I am using devise, omniauth and fbgraph gems, how could I perform this?
Thoughts:
Obtain a new access token. How do I do this? Would I have to modify my omniauth_callbacks_controller?
Should I store the friends list in a session variable?
My omniauth controller:
def facebook
#user = User.find_for_facebook_oauth(request.env["omniauth.auth"], current_user)
if #user.persisted?
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook"
sign_in_and_redirect #user, :event => :authentication
else
session["devise.facebook_data"] = env["omniauth.auth"]
redirect_to new_user_registration_url
end
hav a look at this..
https://developers.facebook.com/roadmap/offline-access-removal/#extend_token

How/why does a user sign_in integration test actually allow a successful fake user sign_in?

I am fairly new to rails. I am working through railstutorial (Rails 3.0). At one point, we write an integration test for the user sign in/sign out flow:
describe "success" do
it "should sign a user in and out" do
user = Factory(:user)
visit signin_path
fill_in :email, :with => user.email
fill_in :password, :with => user.password
click_button
controller.should be_signed_in
click_link "Sign out"
controller.should_not be_signed_in
end
end
My understanding is that testing uses the db/test database. This database is empty, as far as I can tell (opened it using rails console test). How is it that we can create a fake user using Factory, and then sign successfully with that user's name and email?
RailsTutorial explains:
Here the before(:each) block signs in by visiting the signin page and
submitting a valid email/password pair.
Why is it a valid pair? I am clearly missing something.
I am particularly interested in this because it looks to me like the sign in is NOT working, and I would like to figure out why.
You're right, your test database is empty, because each test example is run inside a transaction, and all records created within current example are deleted via rollback. The purpose for that is to keep each example isolated from another one.
But when current example is runnning, your database is populated with records (one record in your case) made by FactoryGirl. Whatever email and password will be generated in factory, it will be persisted both in database and in memory (user variable). That's why user.password will always be correct.

Rails 3 and Devise: authenticating a user while performing a POST action

Using Rails 3.0.6, Omniauth 0.2.0 and Devise 1.2.1, I'm encountering the following situation:
I want to offer users the option to authenticate via Facebook. I have a user system set up using Devise and I can successfully auth using Facebook. I've spent several hours trying to code the behavior I want for one specific situation:
user is not logged in
user has a site account
user authenticates via Facebook
I offer the user 2 choices at this point
create an account (can be a dummy account with no provided info)
link this Facebook authentication with an existing account
I'm having trouble with the latter option. The user has already authenticated but I still need him to log in with his site account. I have an action in my AuthenticationsController that will associate this authentication with a logged in user. Devise doesn't seem to offer a way for me to log the user in while staying in the same action, though. This was my first attempt to do this
class AuthenticationsController < ApplicationController
before_filter :authenticate_user!, :only => :auth_link_existing_user
...
def auth_link_existing_user
...
end
However, using this method, if the user logs in, they're simply redirected to my site's root page. I know I can change Devise's sign-in redirect, but that will be for all sign-ins. I wanted only this situation to have a separate redirect.
After reading through this mailing list question, I tried to extend SessionsController with my own custom behavior:
def create
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
if params[:redirect] #new
redirect_to params[:redirect].to_sym #new
else
respond_with resource, :location => redirect_location(resource_name, resource)
end
end
This doesn't work either. I've defined my auth_link_existing_user route to use a POST verb (which seems accurate) and redirects can only be GETs.
So now I do have a solution in mind: copy and paste code from Devise's authenticate_user! helper into a new function which can be called within a controller action without redirecting. This seems less than ideal to me because it's duplication of code and increases coupling--a Devise or Warden update that changes this behavior will break my code as well.
Has anyone else tried something like this and come up with a more elegant solution? Do you see a simpler way for me to offer this or similar behavior to my users?
UPDATE: For anyone who wants to use my dirty solution at the end, this is what I did:
def auth_link_existing_user
# FROM Devise/sessions/create
resource = warden.authenticate!(:scope => :user, :recall => "registrations#auth_new")
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(:user, resource)
# method defined in Ryan Bates' Railscast for Omniauth w/Devise
current_user.apply_omniauth(session[:omniauth])
current_user.save
end
note that this action MUST be placed in your sessions controller. If not, Warden will give you an "invalid email/password" error. It was an incredibly long debugging process to find the source.
With this in place, I use a login form to submit to this action after the user has authenticated.
I like how clean your solution is, though it goes deeper into the stack.
Here is how I've implemented something similar by following the Devise+Omniauth Facebook example on the Devise wiki and modifying the facebook method to pass on the session information to the Login form, with something like this:
if #user.persisted?
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook"
sign_in_and_redirect #user, :event => :authentication
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
redirect_to new_user_session_url
end
Then, in your case, I'd check in Login controller action for session["devise.facebook_data"], submit the uid + token with the form and apply_omniauth if present.

Mocks aren't working with RSpec and Devise

I'm working on a Rails 3 web app at the moment with RSpec 2 and we're using Devise for authentication. One (and soon many) of our controllers require the user to be logged in. I know Devise provides the sign_in test helper, but can it be used with an RSpec or Mocha mock object?
I originally tried #user = mock_model(User) where user is the Devise class. This wouldn't work with sign_in :user, #user as get 'index' would redirect to the sign in form.
Does anyone have any experience testing with Devise and can help?
We had a similar problem, but using Factory Girl. We solved it like so:
In spec_helper.rb:
config.include Devise::TestHelpers, :type => :controller
In the controller spec (just a wrapper method):
def login_user(user)
sign_in user
end
Then in each method you require, you can do:
login_user(Factory(:user))
... where you have defined a user object in factories.rb. Not sure if this will work with mocks though.
A mock is never going to work. When you say sign in, the user is stored in session (basically, the user class and its id). When you access the controller, another user object is retrieved based on the stored data. The best way to solve the problem is using something that persists the object, like Factory Girl.
I hit the same issue. I'm doing the following for now:
before(:each) do
# sign_in mock_user
request.env['warden'] = mock(Warden, :authenticate => mock_user,
:authenticate! => mock_user)
end
I've created an issue for this here: https://github.com/plataformatec/devise/issues#issue/928
Go vote!
None of them worked for me (MRI 1.9.3-preview1, rails 3.0.1.rc5).
this is the solution i found : http://blog.joshmcarthur.com/post/6407481655/integration-tests-with-devise-and-rspec