I'm trying to test my User model's class method #registered_but_not_logged_in(email), which grabs the first user that matches the email that has a confirmed_at entry but has never logged in (which I'm counting with sign_in_count). I'm using rspec with Factorygirl, plus shoulda-matchers v2.8.
Here's the ruby:
def self.registered_but_not_logged_in email
self.with_email( email ).confirmed.never_signed_in.first
end
I've tested this in the Rails console and I know it works as expected so it's not a logic problem on that end, so I'm thinking I'm doing something wrong in my test:
describe User do
# create #user
describe ".registered_but_not_logged_in" do
it "returns a user that matches the provided email who is confirmed, but who has not yet signed in" do
#user.confirmed_at = 2.days.ago
#user.email = "fisterroboto5893#mailinator.com"
result = described_class.registered_but_not_logged_in("fisterroboto5893#mailinator.com")
expect(result).to be_instance_of(User)
end
end
In this example, result is nil. I know that this is a case of #user existing outside the database while the method is actively checking the DB, but I don't know how to handle this while using rspec/factorygirl. Any help is definitely appreciated!
So I'm not 100% sure why what I'm doing is working, but here's the solution that I stumbled across with the help of #nort and one of my coworkers:
it "returns a user that matches the provided email who is confirmed, but who has not yet signed in" do
#user.confirmed_at = 2.days.ago
#user.email = "fisterroboto5893#mailinator.com"
#user.sign_in_count = 0
#user.save!
expect(User.registered_but_not_logged("fisterroboto5893#mailinator.com")).to be_instance_of(User)
end
What I believe is happening is the save! is saving #user to the test database, which is otherwise completely unpopulated as I develop against a different DB. The issue of course being that we can't test data that doesn't exist.
As a bonus, note that expect().to... is the preferred convention for rpsec. Also, described_class I believe would totally work fine, but am preferring explicitness right now since I'm still learning this stuff.
Say I want to test that a User is able to fill a form to create a new project. The route that I need to visit is: new_user_project_path(:user_id).
So, my feature rspec looks like this right now:
feature "User creates a project" do
scenario "Logged in User creates a project" do
login_with_oauth #this sets current_user
visit new_user_project_path
fill_in 'Title', :with => 'Colchasdadasdasoneta'
fill_in 'Description', :with => 'lalalssalsalas'
click_in 'Create Project'
end
end
As you can tell, I can't use this visit new_user_project_path because it needs a :user_id to be passed. My question is, how can I access the current_user? Or what is the correct way of testing this kind of stuff?
Your routes have problem. Instead of fixing the problem directly which is easy, better to double check your code logic.
You should not define user id as path param to create a new project. Instead, the project instance should be initiated by current_user.
To associate a new project with user, just do it in controller
#project = current_user.projects.new
In conclusion:
Change the controller code similar to above
Restore your routes to conventional resource.
Then, use your test as it is, though a bit more expectations should be appended.
Assuming login_with_oauth is a spec helper method that you wrote, you can have that return the user that it setup. Then you can do:
feature "User creates a project" do
scenario "Logged in User creates a project" do
user = login_with_oauth
visit new_user_project_path(user)
If that doesn't work, and login_with_oauth is something you don't control (say it comes from some library), you can fetch the first user out of the database. This is assuming no other users are in the db, but there shouldn't be because your tests should be clean and autonomous. Then do:
feature "User creates a project" do
scenario "Logged in User creates a project" do
login_with_oauth
user = User.first
visit new_user_project_path(user)
I've put together a basic application with user authentication using bcrypt-ruby and has_secure_password. The result is essentially a barebones version of the application from the Rails Tutorial. In other words, I have a RESTful user model as well as sign-in and sign-out functionality.
As part of the tests for editing a user's information, I've written a test for changing a password. Whereas changing the password works just fine in the browser, my test below is not passing.
subject { page }
describe "successful password change"
let(:new_password) { "foobaz" }
before do
fill_in "Password", with: new_password
fill_in "Password Confirmation", with: new_password
click_button "Save changes"
end
specify { user.reload.password.should == new_password }
end
Clearly, I'm misunderstanding some basic detail here.
In short:
1) Why exactly is the code above not working? The change-password functionality works in the browser. Meanwhile, rspec continues to reload the old password in the last line above. And then the test fails.
2) What is the better way to test the password change?
Edit:
With the initial password set to foobar, the error message is:
Failure/Error: specify { user.reload.password.should == new_password }
expected: "foobaz"
got: "foobar" (using ==)
Basically, it looks like the before block is not actually saving the new password.
For reference, the related controller action is as follows:
def update
#user = User.find(params[:id])
if #user.update_attributes(params[:user])
flash[:success] = "Profile Updated"
sign_in #user
redirect_to root_path
else
render 'edit'
end
end
For Devise users, use #valid_password? instead:
expect(user.valid_password?('correct_password')).to be(true)
Credit: Ryan Bigg
One not so satisfying solution here is to write a test using the #authenticate method provided by bcrypt-ruby.
specify { user.reload.authenticate(new_password).should be_true }
Granted this isn't a proper integration test, but it will get us to green.
Your answer (using authenticate) is the right approach; you should be satisfied with it. You want to compare the hashed versions of the passwords not the #password (via attr_accessor) in the model. Remember that you're saving a hash and not the actual password.
Your user in your test is an copy of that user in memory. When you run the tests the update method loads a different copy of that user in memory and updates its password hash which is saved to the db. Your copy is unchanged; which is why you thought to reload to get the updated data from the database.
The password field isn't stored in the db, it's stored as a hash instead, so the new hash gets reloaded from the db, but you were comparing the ephemeral state of #password in your user instance instead of the the encrypted_password.
I use db/seeds.rb to populate my database with 2 user roles ("Admin", "User") that will never change. When i run tests though, the seed data does not get carried over and the results are error tests.
When i try to run cucumber i get the following error:
Using the default profile... Feature: Sign in In order to get access
to protected sections of the site A valid user Should be able to
sign in
Scenario: User is not signed up #
features/users/sign_in.feature:6 Not registered: role
(ArgumentError)
/Users/x/.rvm/gems/ruby-1.9.2-p180/gems/factory_girl-2.0.0.rc4/lib/factory_girl/registry.rb:15:in
find'
/Users/x/.rvm/gems/ruby-1.9.2-p180/gems/factory_girl-2.0.0.rc4/lib/factory_girl.rb:39:in
factory_by_name'
/Users/x/.rvm/gems/ruby-1.9.2-p180/gems/factory_girl-2.0.0.rc4/lib/factory_girl/syntax/vintage.rb:53:in
default_strategy'
/Users/x/.rvm/gems/ruby-1.9.2-p180/gems/factory_girl-2.0.0.rc4/lib/factory_girl/syntax/vintage.rb:146:in
Factory'
/Users/x/rails/ply_rails/features/support/db_setup.rb:6:in
`Before'
Given I am not logged in #
features/step_definitions/user_steps.rb:36
Here is what my setup looks like:
# features/support/db_setup.rb
Before do
# Truncates the DB before each Scenario,
# make sure you've added database_cleaner to your Gemfile.
DatabaseCleaner.clean
Factory(:role, :name => 'Admin')
Factory(:role, :name => 'User')
end
# features/users/sign_in.feature
Feature: Sign in
In order to get access to protected sections of the site
A valid user
Should be able to sign in
Scenario: User is not signed up # THIS IS LINE 6
Given I am not logged in
And no user exists with an email of "user#user.com"
When I go to the sign in page
And I sign in as "user#user.com/password"
Then I should see "Invalid email or password."
And I go to the home page
And I should be signed out
# features/step_definitions/user_steps.rb
Given /^I am a "([^"]*)" named "([^"]*)" with an email "([^"]*)" and password "([^"]*)"$/ do |role, name, email, password|
User.new(:name => name,
:email => email,
:role => Role.find_by_name(role.to_s.camelize),
:password => password,
:password_confirmation => password).save!
end
Not sure where to start on getting this working, thank you for your help/time!
Well the point of tests is to start with a clean database, i.e a consistent state, so it's kind of good that everything gets wiped.
Secondly, in terms of cucumber, you should be defining a Background block to do the set up. This will run for each scenario, and has the benefit of every action being explicitly known. This is especially useful if you use the cucumber scenario plain text to show to clients. So you should do something like:
Background:
Given that the role "Admin" exists
And that the role "User" exists
Scenario:
etc
And make custom steps for the that the role [blank] exists that will create the role for you.
I am working on some Cucumber stories for a 'sign up' application which has a number of steps.
Rather then writing a Huuuuuuuge story to cover all the steps at once, which would be bad, I'd rather work through each action in the controller like a regular user. My problem here is that I am storing the account ID which is created in the first step as a session variable, so when step 2, step 3 etc are visited the existing registration data is loaded.
I'm aware of being able to access controller.session[..] within RSpec specifications however when I try to do this in Cucumber stories it fails with the following error (and, I've also read somewhere this is an anti-pattern etc...):
Using controller.session[:whatever] or session[:whatever]
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)
Using session(:whatever)
wrong number of arguments (1 for 0) (ArgumentError)
So, it seems accession the session store isn't really possible. What I'm wondering is if it might be possible to (and I guess which would be best..):
Mock out the session store etc
Have a method within the controller and stub that out (e.g. get_registration which assigns an instance variable...)
I've looked through the RSpec book (well, skimmed) and had a look through WebRat etc, but I haven't really found an answer to my problem...
To clarify a bit more, the signup process is more like a state machine - e.g. the user progresses through four steps before the registration is complete - hence 'logging in' isn't really an option (it breaks the model of how the site works)...
In my spec for the controller I was able to stub out the call to the method which loads the model based on the session var - but I'm not sure if the 'antipattern' line also applies to stubs as well as mocks?
Thanks!
I'll repeat danpickett in saying mocks should be avoided whenever possible in Cucumber. However if your app does not have a login page, or perhaps performance is a problem, then it may be necessary to simulate login directly.
This is an ugly hack, but it should get the job done.
Given /^I am logged in as "(.*)"$/ do |email|
#current_user = Factory(:user, :email => email)
cookies[:stub_user_id] = #current_user.id
end
# in application controller
class ApplicationController < ActionController::Base
if Rails.env.test?
prepend_before_filter :stub_current_user
def stub_current_user
session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
end
end
end
mocks are bad in cucumber scenarios - they're almost kind of an antipattern.
My suggestion is to write a step that actually logs a user in. I do it this way
Given I am logged in as "auser#example.com"
Given /^I am logged in as "(.*)"$/ do |email|
#user = Factory(:user, :email => email)
#user.activate!
visit("/session/new")
fill_in("email", :with => #user.email)
fill_in("password", :with => #user.password)
click_button("Sign In")
end
I realize that the instance variable #user is kind of bad form—but I think in the case of logging in/out, having #user is definitely helpful.
Sometimes I call it #current_user.
Re. Ryan's solution - you can open up ActionController in you env.rb file and place it there to avoid putting in your production code base (thanks to john # pivotal labs)
# in features/support/env.rb
class ApplicationController < ActionController::Base
prepend_before_filter :stub_current_user
def stub_current_user
session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
end
end
I don't know how much this relates to the original question anymore, but I decided to post anyway in the spirit of discussion...
We have a cucumber test suite that takes > 10 minutes to run so we wanted to do some optimization. In our app the login process triggers a LOT of extra functionality that is irrelevant to majority of the scenarios, so we wanted to skip that by setting the session user id directly.
Ryanb's approach above worked nicely, except that we were unable to log out using that approach. This made our multi-user stories fail.
We ended up creating a "quick login" route that is only enabled in test environment:
# in routes.rb
map.connect '/quick_login/:login', :controller => 'logins', :action => 'quick_login'
Here is the corresponding action that creates the session variable:
# in logins_controller.rb
class LoginsController < ApplicationController
# This is a utility method for selenium/webrat tests to speed up & simplify the process of logging in.
# Please never make this method usable in production/staging environments.
def quick_login
raise "quick login only works in cucumber environment! it's meant for acceptance tests only" unless Rails.env.test?
u = User.find_by_login(params[:login])
if u
session[:user_id] = u.id
render :text => "assumed identity of #{u.login}"
else
raise "failed to assume identity"
end
end
end
For us this ended up being simpler than working with the cookies array. As a bonus, this approach also works with Selenium/Watir.
Downside is that we're including test-related code in our application. Personally I don't think that adding code to make application more testable is a huge sin, even if it does add a bit of clutter. Perhaps the biggest problem is that future test authors need to figure out which type of login they should use. With unlimited hardware performance we obviously wouldn't be doing any of this.
Re: Ryan's solution:
Does not work with Capybara, unless small adaptation done:
rack_test_driver = Capybara.current_session.driver
cookie_jar = rack_test_driver.current_session.instance_variable_get(:#rack_mock_session).cookie_jar
#current_user = Factory(:user)
cookie_jar[:stub_user_id] = #current_user.id
(found here: https://gist.github.com/484787)
My understanding is that you get:
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)
when session[] is accessed before request has been instantiated. In your case, I'd imagine if you put webrats' visit some_existing_path before accessing session[] in your step defenition, the error will go away.
Now, unfortunately, session doesn't seem to persist across steps (at least, I couldn't find the way), so this bit of information doesn't help to answer your question :)
So, I suppose, Ryan's session[:user_id] = cookies[:stub_user_id]... is the way to go. Although, imo, test related code in the application itself doesn't sound right.
I use a testing-only sign-in solution like Prikka's, but I do it all in Rack instead of creating a new Controller and routes.
# in config/environments/cucumber.rb:
config.middleware.use (Class.new do
def initialize(app); #app = app; end
def call(env)
request = ::Rack::Request.new(env)
if request.params.has_key?('signed_in_user_id')
request.session[:current_user_id] = request.params['signed_in_user_id']
end
#app.call env
end
end)
# in features/step_definitions/authentication_steps.rb:
Given /^I am signed in as ([^\"]+)$/ do |name|
user = User.find_by_username(name) || Factory(:user, :username => name)
sign_in_as user
end
# in features/step_definitions/authentication_steps.rb:
Given /^I am not signed in$/ do
sign_in_as nil
end
module AuthenticationHelpers
def sign_in_as(user)
return if #current_user == user
#current_user = user
get '/', { 'signed_in_user_id' => (user ? user.to_param : '') }
end
end
World(AuthenticationHelpers)
#Ajedi32 I ran into the same issue (undefined method 'current_session' for Capybara::RackTest::Driver) and putting this in my step definition fixed the problem for me:
rack_test_browser = Capybara.current_session.driver.browser
cookie_jar = rack_test_browser.current_session.instance_variable_get(:#rack_mock_session).cookie_jar
cookie_jar[:stub_user_id] = #current_user.id
In my controller action, I referred to cookies[:stub_user_id], instead of cookie_jar[:stub_user_id]
Why don't you use FactoryGirl or (Fixjour or Fabricator) with Devise (or Authlogic) and SentientUser? Then you can simply sniff which user is already logged in!
#user = Factory(:user) # FactoryGirl
sign_in #user # Devise
User.current.should == #user # SentientUser
Another slight variation:
# In features/step_definitions/authentication_steps.rb:
class SessionsController < ApplicationController
def create_with_security_bypass
if params.has_key? :user_id
session[:user_id] = params[:user_id]
redirect_to :root
else
create_without_security_bypass
end
end
alias_method_chain :create, :security_bypass
end
Given %r/^I am logged in as "([^"]*)"$/ do |username|
user = User.find_by_username(username) || Factory(:user, :username => username)
page.driver.post "/session?user_id=#{user.id}"
end
After a lot of soul searching and web surfing, I finally opt'ed for a very simple and obvious solution.
Using cookies adds two problems. First you have code in the application specific for testing and second there is the problem that creating cookies in Cucumber is hard when using anything other than rack test. There are various solutions to the cookie problem but all of them are a bit challenging, some introduce mocks, and all of them are what I call 'tricky'. One such solution is here.
My solution is the following. This is using HTTP basic authentication but it could be generalized for most anything.
authenticate_or_request_with_http_basic "My Authentication" do |user_name, password|
if Rails.env.test? && user_name == 'testuser'
test_authenticate(user_name, password)
else
normal_authentication
end
end
test_authenticate does what ever the normal authenticate does except it bypasses any time consuming parts. In my case, the real authentication is using LDAP which I wanted to avoid.
Yes… it is a bit gross but it is clear, simple, and obvious. And… no other solution I've seen is cleaner or clearer.
Note, one feature is that if the user_name is not 'testuser', then the normal path is taken so they can be tested.
Hope this helps others...