testing custom devise and/or warden strategy with rspec - ruby-on-rails

I see plenty that lead to custom authorization strategies for devise and warden, but what I'm sepcifically after is testing these solutions with rspec. Similar to this question: Filtering users who are able to sign in with Devise
What can I do to test this sort of implementation. Rails 3.1.1, Devise (most current), etc.

For those that may do this in the future, here is my solution:
This is the class that sets a new strategy for authentication through Devise (and it could also be used with Warden with a few small changes).
require 'devise/strategies/authenticatable'
module Devise
module Strategies
class AndroidAuthenticatable < Authenticatable
def valid?
# return true/false
return valid_params? && valid_headers?
end
def authenticate!
failure_message = "Authentication failed for device/user"
klass = mapping.to # if we're going to come here, we can mock this
begin
# code to determine whether or not to authenticate this request
# if yes, success!(instance of klass)
# if no, fail!(messsage)
rescue
fail!(failure_message) # always fail if not success
end
end
protected
def valid_params?
# params that show whether request should be here
end
def valid_headers?
# headers that determine if request should be here
end
end
end
end
The previous class is in my lib/.../strategies directory. I also have lib configured for auto-loading through the rails configuration.
From the rspec side, after I created the above class I write out a few stubs/mocks. Here is a basic rspec file to get you started.
# I do this in before block or right before test executes
#request = mock(:request)
#strategy = Devise::Strategies::AndroidAuthenticatable.new(nil)
#request.should_receive(:headers).and_return({#hash of the headers you are testing})
#strategy.should_receive(:params).at_least(:once).and_return({#hash of the params})
#strategy.should_receive(:request).and_return(#request)
# Break these up as needed to test failing and successful
# strategies for your application
lambda {
#strategy.should be_valid
#strategy.authenticate!.should eql :success
}.should_not raise_error
This isn't all inclusive, but I feel it should get us a good head start when adding strategies with Warden or Devise. I actually had to implement what I thought would work and then right tests to prove it after the fact. Now we can do it the other way around perhaps.

Related

Rails 6 ActionController::TestCase stub a controller method

I am new with Ruby/Rails and the testing frameworks within Ruby/Rails. I have a pre-validation method (external API) that validates the incoming request. For all test cases, I want to stub that call and test the remaining functionalities.
I am knowledgeable about testing and mocks/stubs/spies (mostly Mockito/Powermockito stuffs), but do not know my way around Rails testing. I tried looking into RSpec / MiniTest stuffs, but it is getting overwhelming.
I have a controller method like this:
def handler
# validate
request_validated = validate_request
unless request_validated
head :unauthorized
return
end
#... remaining codes
end
def validate_request
# validation with external API
end
I have controller tests set up using ActionController::TestCase. Prior to adding the validation stuffs, all my test cases have tested out. But I cannot stub around the validation check.
I would want to have something like
controller.stub(validate_request).then_and_return(true) # need something of this sort
post :handler, as: :json, params: completed_service_parameters
assert_response :no_content
I'm open to using any library, though would prefer to use any Rails in-built, if there's anything. Thanks.
I ended up using 'minitest/stub_any_instance'
require 'minitest/stub_any_instance'
...
test 'test stub' do
...
Controller.stub_any_instance(:function, return-value) do
# perform the call within the stubbed block
post :function, as: :json, params: { :param1 => 'random' }
end
...
end

How to avoid ActionMailer::Preview committing data to development database?

I'm using Rails 4.1.0.beta1's new Action Mailer previews and have the following code:
class EventInvitationPreview < ActionMailer::Preview
def invitation_email
invite = FactoryGirl.create :event_invitation, :for_match, :from_user, :to_user
EventInvitationMailer.invitation_email(invite)
end
end
This is all good until I actually try to preview my email and get an error saying that validation on a User object failed due to duplicate email addresses. Turns out that ActionMailer::Preview is writing to my development database.
While I could work around the validation failure or use fixtures instead of factories, is there any way to avoid ActionMailer::Preview writing to the development database, e.g. use the test database instead? Or am I just doing it wrong?
Cleaner/Easier (based on other answers) and tested with Rails 7: Do not change Rails' classes but create your own. Id addition to not change the controller but the call method of ActionMailer::Preview.
# app/mailers/preview_mailer.rb
class PreviewMailer < ActionMailer::Preview
def self.call(...)
message = nil
ActiveRecord::Base.transaction do
message = super(...)
raise ActiveRecord::Rollback
end
message
end
end
# inherit from `PreviewController` for your previews
class EventInvitationPreview < PreviewController
def invitation_email
...
end
end
OLD:
You can simply use a transaction around email previews, just put this inside your lib/monkey_mailers_controller.rb (and require it):
# lib/monkey_mailers_controller.rb
class Rails::MailersController
alias_method :preview_orig, :preview
def preview
ActiveRecord::Base.transaction do
preview_orig
raise ActiveRecord::Rollback
end
end
end
Then you can call .create etc. in your mailer previews but nothing will be saved to database. Works in Rails 4.2.3.
A cleaner way to proceed is to prepend a module overriding and wrapping preview into a transaction:
module RollbackingAfterPreview
def preview
ActiveRecord::Base.transaction do
super
raise ActiveRecord::Rollback
end
end
end
Rails.application.config.to_prepare do
class Rails::MailersController
prepend RollbackingAfterPreview
end
end
TL;DR -- The original author of the ActionMailer preview feature (via the MailView gem) provides three examples of different supported approaches:
Pull data from existing fixtures: Account.first
Factory-like pattern: user = User.create! followed by user.destroy
Stub-like: Struct.new(:email, :name).new('name#example.com', 'Jill Smith')
~ ~ ~ ~ ~ ~ ~ ~ ~ ~
To elaborate on the challenge faced by the OP...
Another manifestation of this challenge is attempting to use FactoryGirl.build (rather than create) to generate non-persistent data. This approach is suggested by one of the top Google results for "Rails 4.1" -- http://brewhouse.io/blog/2013/12/17/whats-new-in-rails-4-1.html?brewPubStart=1 -- in the "how to use this new feature" example. This approach seems reasonable, however if you're attempting to generate a url based on that data, it leads to an error along the lines of:
ActionController::UrlGenerationError in Rails::Mailers#preview
No route matches {:action=>"edit", :controller=>"password_resets", :format=>nil, :id=>nil} missing required keys: [:id]
Using FactoryGirl.create (rather than build) would solve this problem, but as the OP notes, leads to polluting the development database.
If you check out the docs for the original MailView gem which became this Rails 4.1 feature, the original author provides a bit more clarity about his intentions in this situation. Namely, the original author provides the following three examples, all focused on data reuse / cleanup / non-persistence, rather than providing a means of using a different database:
# app/mailers/mail_preview.rb or lib/mail_preview.rb
class MailPreview < MailView
# Pull data from existing fixtures
def invitation
account = Account.first
inviter, invitee = account.users[0, 2]
Notifier.invitation(inviter, invitee)
end
# Factory-like pattern
def welcome
user = User.create!
mail = Notifier.welcome(user)
user.destroy
mail
end
# Stub-like
def forgot_password
user = Struct.new(:email, :name).new('name#example.com', 'Jill Smith')
mail = UserMailer.forgot_password(user)
end
end
For Rails 6:
#Markus' answer worked for me, except that it caused a nasty deprecation-soon-will-be-real error related to how Autoloading has changed in Rails 6:
DEPRECATION WARNING: Initialization autoloaded the constants [many constants seemingly unrelated to what I actually did]
Being able to do this is deprecated. Autoloading during initialization is going to be an error condition in future versions of Rails.
[...]
Well, that's no good!
After more searching, this blog and the docs for
to_prepare helped me come up with this solution, which is just #Markus' answer wrapped in to_prepare. (And also it's in initializer/ instead of lib/.)
# /config/initializers/mailer_previews.rb
---
# Wrap previews in a transaction so they don't create objects.
Rails.application.config.to_prepare do
class Rails::MailersController
alias_method :preview_orig, :preview
def preview
ActiveRecord::Base.transaction do
preview_orig
raise ActiveRecord::Rollback
end
end
end
end
If you have a complicated object hierarchy, you can exploit transactional semantics to rollback the database state, as you would in a test environment (assuming your DB supports transactions). For example:
# spec/mailers/previews/price_change_preview.rb
class PriceChangeMailerPreview < ActionMailer::Preview
#transactional strategy
def price_decrease
User.transaction do
user = FactoryGirl.create(:user, :with_favorited_products) #creates a bunch of nested objects
mail = PriceChange.price_decrease(user, user.favorited_products.first)
raise ActiveRecord::Rollback, "Don't really want these objects committed to the db!"
end
mail
end
end
#spec/factories/user.rb
FactoryGirl.define do
factory :user do
...
trait :with_favorited_products do
after(:create) do |user|
user.favorited_products << create(:product)
user.save!
end
end
end
end
We can't use user.destroy with dependent: :destroy in this case because destroying the associated products normally doesn't make sense (if Amazon removes me as a customer, they don't remove all the products I have favorited from the market).
Note that transactions are supported by previous gem implementations of the preview functionality. Not sure why they aren't supported by ActionMailer::Preview.

Devise ignore my custom strategy

I want to create a custom auth strategy for accessing the API. I followed the example code at Devise ignoring custom strategy.
The problem is that the valid? method in my Api strategy is never run (based on trying to pry in it).
My code:
module Devise
module Strategies
class Api < Devise::Strategies::Base
def valid?
binding.pry
params[:request_source] == 'api'
end
def authenticate!
#do stuff here
if user
success!(user)
else
warden.custom_failure!
render :json=> {:success=>false, :message=>"Error with your login or password"}, :status=>401
end
end
end
Warden::Strategies.add(:api, Devise::Strategies::Api)
end
end
and in the devise initializer:
config.warden do |manager|
manager.default_strategies.unshift :api
end
What ever I do, it seems like Devise always use its default strategy. AFAIK, this should be enough...
-------EDIT--------
I require the strategy like this at the very top of my devise initializer:
require Rails.root.join('app/devise/strategies/api')
I know the strategy is loaded at boot time since if I put a pry call inside the class, it will start a pry session. But the Pry calls inside the methods are never run. :-S
Found the answer!
I had to use this:
config.warden do |manager|
manager.default_strategies(scope: :user).unshift :api
end
to make it work. Weird thing is, a lot of the sample code I saw on the net did not use it :-S
When are you requiring your custom strategy? Comparing to the example, you are adding the strategy there instead of in your initializer. You might try requiring the custom strategy at the top of your initializer to make sure it's loaded before you add it to default_strategies.
If that doesn't do the trick, don't be afraid to add some temporary puts statements right in devise itself where authenticate is called to check default_strategies. That is, if you're not already confortable using the debugger, which is what I would do.

Session variables with Cucumber Stories

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...

How do I fake OpenID login in RSpec user story/Cucumber when using open_id_authentication plugin

I'm trying to write a Cucumber scenario that requires me to have a logged in user - that would normally be quite simple but I'm only using OpenID authentication (curtosy of the authentication plugin). However after digging through the open_id_authentication plugins guts I'm not sure how I could achieve this within Cucumber.
I've figured out a way, if you place this in your features/support/env.rb:
ActionController::Base.class_eval do
private
def begin_open_id_authentication(identity_url, options = {})
yield OpenIdAuthentication::Result.new(:successful), identity_url, nil
end
end
Then you can just do something like this in your appropriate step:
Given /^I am logged in as "(.*)"$/ do |name|
user = User.find_by_name(user)
post '/session', :openid_url => user.identity_url
# Some assertions just to make sure our hack in env.rb is still working
response.should redirect_to('/')
flash[:notice].should eql('Logged in successfully')
end
I'm just completely clobbering the open id auth for the cucumber features, obviously if I need instances where there is failed login I could do that based on the supplied identity_url.
If you want to be able to stub out responses do this:
In features/support/helpers.rb:
ActionController::Base.class_eval do
private
def fake_openid_response(identity_url)
[OpenIdAuthentication::Result.new(:successful), identity_url, nil]
end
def begin_open_id_authentication(identity_url, options = {})
yield fake_openid_response(identity_url)
end
end
By moving the response out to a separate method you can now stub the response in your steps if necessary. For example, if I wanted a :missing response and I had a controller GoogleLoginController I could do the following using Mocha:
GoogleLoginController.any_instance.stubs(:fake_openid_response)
.returns([OpenIdAuthentication::Result.new(:missing), identity_url, nil])
Bort, a rails skeleton app, has a full set of rspec tests and supports openID login so you may want to take a look and see what they do.
DEfusion's answer works except that I needed to normalize the identity_url like:
ActionController::Base.class_eval do
private
def begin_open_id_authentication(identity_url, options = {})
yield OpenIdAuthentication::Result.new(:successful), self.normalize_identifier(identity_url), nil
end
end
Thanks

Resources