I'm trying to check if my sessions works well with basic authentication. Here is my controller :
class ClientsController < ApplicationController
skip_before_filter :verify_authenticity_token
before_action :authenticate
def create
#client = Client.create!({
:user_id => #current_user.id
})
session[:client_id] = #client.id
render(:xml => { :status => 'OK' })
end
private
def authenticate
authenticate_or_request_with_http_basic do |username, password|
# User checking...
#current_user = checked_user
end
end
end
end
It's a very basic controller. But when I try to see if session[:client_id] is correctly set, it's just returning nil.
I didn't write the initialization of #user.
it "should create session" do
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(#user.login, #password)
post :create
response.should be_success # not fail
Hash.from_xml(response.body)['hash']['status'].should == 'OK' # not fail
Client.last.user.should == #user # not fail
assigns(session[:client_id]).should == Client.last.id # Fail !
end
The error is that assigns(session[:client_id]) is nil... I'm totally sure the #client is initialized and the render is OK, but session seems not to be saved.
It's the first time I use rspec with session. Is it the correct writing of this test ?
Regards
So the issue is the line:
assigns(session[:client_id]).should == Client.last.id # Fail !
assigns is a method that is going to point to the equivalent instance method, so assigns(session[:client_id]) is going to check for #session[:client_id], which it won't find.
Also, the session hash is available in rspec so you can call it like you would in your controller, which is what you need to do here:
session[:client_id].should == Client.last.id # pass
Related
In ApplicationController, according to devise docs, How To: Redirect to a specific page on successful sign in and sign out, the case switch when can not be reached, even in pry debugging console, it shows 'resource.class == User is true'. I don't know what part of Rails processing I missed, any hint will be appreciated!
# ApplicationController.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
protected
def after_sign_in_path_for(resource)
# check for the class of the object to determine what type it is
binding.pry
case resource.class
when User
puts "user redirect ==== "
return session.delete(:return_to) || current_user_path
else
puts "super call ....."
super
end
end
end
You are pretty close. Just need to get the resource class name using resource.class.name , so that you can compare it with a string such as 'User' which is nothing but your class name.
def after_sign_in_path_for(resource)
# check for the class of the object to determine what type it is
binding.pry
case resource.class.name #=>this would return the class name i.e 'User'
when 'User'
puts "user redirect ==== "
return session.delete(:return_to) || current_user_path
else
puts "super call ....."
super
end
end
You can make workaround by creating SessionsController that inherits from Devise::SessionsController.
class SessionsController < Devise::SessionsController
skip_before_filter :authenticate_user!
def create
user = User.find_for_database_authentication(email: params[:session][:email])
if user && user.valid_password?(params[:session][:password])
sign_in user
redirect_to session.delete(:return_to) || '/authorized'
else
redirect_to '/sign_in'
end
end
def destroy
sign_out :user
redirect_to '/signed_out'
end
end
Point to it inside your routes.rb like this:
devise_for :users, controllers: {sessions: 'sessions'}
Even a seemingly simple index action feels incredibly complicated to test in isolation.
I find myself having to mock out several of my User and Tenant methods just to get through the before_filters. Then I need to mock out Kaminari and Tenant#users for the action.
This feels excessive for testing a controller action with no control flow.
TDD principle would say that an excessive need for mocking is a sign of poor design, but then I'm not sure how I would extract this functionality into a domain object.
Is this sort of painful mocking standard for testing Rails controllers? Is there better way to do this that I'm simply not aware of?
For instance, perhaps skipping before_filters would make this less painful, but as they are consequential private methods, I feel that skipping them is missing the point.
class UsersController < AdminController
before_filter :check_auth
before_filter :check_admin
around_filter :set_tenant_time_zone, if: current_tenant
def index
Kaminari.paginate(current_tenant.users).page(params[:page])
end
private
def current_user
# gets user from session
end
def current_tenant
current_user.tenant if current_user
end
def set_tenant_time_zone
Time.use_zone(current_tenant.time_zone, &block)
end
def check_auth
redirect_to login_url unless AuthChecker.new(current_user, request.remote_ip).has_access?
end
def check_admin
redirect_to root_url unless current_user.is_admin?
end
end
You have to do all those mocks/stubs if you want to run those before_filters but I think, that, for those cases, is better to use some spec helper method to create a logged in user so, on your spec, you only need to call that method on a "before(:each)" block of your controller where you want a user.
In spec_helper.rb:
def current_user(stubs = {})
unless #current_user
u = FactoryGirl.build(:user, stubs)
u.save(:validate => false)
#current_user = u
end
#current_user
end
def current_user_session(stubs = {}, user_stubs = {})
#current_session ||= mock_model("Session", {:record => nil, :user => current_user(user_stubs)}.merge(stubs))
end
def login(session_stubs = {}, user_stubs = {})
UserSession.stub(:find).and_return(current_user_session(session_stubs, user_stubs))
controller.stub(:current_user => #current_user)
end
so, on the controller specs that require a logged in user with some special stub I can do
describe 'GET index' do
before(:each) do
login #this does all you need to pass the filters
end
it 'does something' do
current_user.stub(:some_method)
get :index
expect(response).to something
end
end
that way the test only has stubs, instances and expectations for the actual code of the action and not the filters
I am building an application in Rails 2.3.14 using Ruby 1.8.7.
My client has requested a very simple authentication on a webinars page.
I thought using http_auth would be very fitting, as it just needs a very basic username and password.
Now, she has requested that if they hit cancel or use the wrong information, they get redirected to a page that basically says "if you forget login information, contact us."
How do I make it so that when we get the "HTTP Basic: Access denied." error, I can instead redirect to a page? Or, instead, just customize this page with our custom styles/content?
Thanks in advance.
Here is the code from my webinars controller:
class WebinarsController < ApplicationController
before_filter :authenticate, :only => [:bpr]
def bpr
render :action => :bpr
end
protected
def authenticate
authenticate_or_request_with_http_basic do |username, password|
username == "abc" && password == "123"
end
end
end
If you look at the authenticate_or_request_with_http_basic source, you'll see this:
def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure)
authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm)
end
def authenticate_with_http_basic(&login_procedure)
HttpAuthentication::Basic.authenticate(request, &login_procedure)
end
#...
def authenticate(request, &login_procedure)
unless request.authorization.blank?
login_procedure.call(*user_name_and_password(request))
end
end
So your login_procedure block can do pretty much anything it wants as long as it returns true for a successful login. In particular, it can call redirect_to:
def authenticate
authenticate_or_request_with_http_basic do |username, password|
if(username == "abc" && password == "123")
true
else
redirect_to '/somewhere/else/with/instructions'
end
end
end
I've been working through Michael Hartl's Rails tutorial (which is unbelievably awesome by the way).
Anyway, everything has been going pretty well and I've nearly reached the end of chapter 10. The problem is that my rspec tests have started to generate some failures and I can't figure out what's wrong.
The first failure occurred when I was working through the section on destroying users. The test
before :each do
#user = Factory :user
end
describe "as a non-signed-in user" do
it "should deny access" do
delete :destroy, :id => #user
response.should redirect_to(signin_path)
end
end
gives the error:
UsersController DELETE 'destroy' as a non-signed-in user should deny access
Failure/Error: delete :destroy, :id => #user
NoMethodError:
undefined method `admin?' for nil:NilClass
# ./app/controllers/users_controller.rb:76:in `admin_user'
# ./spec/controllers/users_controller_spec.rb:308:in `block (4 levels) in <top (required)>'
Here's the code the message references in users_controller:
def admin_user
# the error tels me that current_user = NilClass
redirect_to(root_path) unless current_user.admin?
end
So I guess this would suggest that current_user isn't working correctly and is being set to nil. Now current_user involves a lot of methods of the SessionsHelper which (afaik) deal with setting the users ID in secure cookies and referencing the cookie as they move around the site. So this suggests that there is something wrong with the cookies.
I've checked the browser and the cookie is being set, I've also gone over every part of the code and it all replicates the tutorial exactly as far as I can tell.
Is there something else I should be looking at?
Appendix
Here is the contents of the SessionsHelper module:
module SessionsHelper
def sign_in user
# rails represents cookies as a hash and deals with the conversion for us
# making the cookie "signed" makes it impervious to attack
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
# this line calls the assignment operator below
self.current_user = user
end
def current_user=(user)
#current_user = user
end
# this is a getter method
def current_user
# this sets #current_user = to the user corresponding to the remember token
# but only if #current user is undefined. ie it only works once
#current_user ||= user_from_remember_token
end
def signed_in?
# return true if current_user is not nil
!current_user.nil?
end
def sign_out
cookies.delete(:remember_token)
self.current_user = nil
end
def current_user? user
# returns true if the user object == the current_user object
user == current_user
end
def authenticate
deny_access unless signed_in?
end
def deny_access
store_location
# this is a shortcut for flash notices: flash[:notice] = "Please sign in to access this page."
redirect_to signin_path, :notice => "Please sign in to access this page."
end
def redirect_back_or(default)
redirect_to(session[:return_to] || default)
clear_return_to
end
private
def user_from_remember_token
# the * allows us to give a 2 element array to a method expecting 2 seperate arguments
User.authenticate_with_salt(*remember_token)
end
def remember_token
# return [nil, nil] if the :remember_token cookie is nil
cookies.signed[:remember_token] || [nil, nil]
end
def store_location
# stores the url the browser was requesting
session[:return_to] = request.fullpath
end
def clear_return_to
session[:return_to] = nil
end
end
In your spec ypu are trying to delete user, while you are not logged in, so current_user is nil. You should prevent access to this action non singed in user.
class UsersController < ApplicationController
before_filter :authenticate, :only => [:index, :edit, :update, :destroy]
....
Just as it is described in example.
I made the same omission and this fixed worked. Thanks. I agree with ducky, Michael Hartl's Rails tutorial is tremendously awesome. It's almost a shame to +just+ call it a "Rails Tutorial." While the tutorial does indeed seem to be pointing me towards good Rails development habits, the text concisely contains much more. Very well done.
I am trying to use REST API, so I want get a #current_user in APP2 from a RoR APP1.
In APP1/config/routes.rb I have this code:
resources :users do
collection do
get 'current'
end
end
In APP1/controllers/application_controller.rb I have this code:
before_filter :current_user
def current_user
if cookies[:remember_me]
current_user = user_from_cookie
else
current_user = User.find_by_id(session[:current_user_id])
end
unless !current_user.nil?
default_current_user = User.find_by_id(1)
end
return #current_user = current_user.nil? ? default_current_user : current_user
end
In APP1/controllers/users_controller.rb I have this code:
def index
...
end
def show
...
end
...
def current
respond_to do |format|
format.xml { render :xml => #current_user }
end
end
In APP2/models/user.rb I have this code:
class User < ActiveResource::Base
self.site = "http://APP1"
end
In APP2/controllers/application_controller.rb I have this code:
before_filter :current_profile
def current_profile
#current_profile = User.get(:current)
end
Now, if I Sign in my User2 in APP1 and I go to http://APP1/users/current.xml URL I get the correct #current_user (User2 object), but if I go to http://APP2/, even though I have 'before_filter's, the #current_profile will be always the default_current_user (User.find_by_id(1) object) instead of User2.
It seems do not care this code from APP1/controllers/application_controller.rb:
if cookies[:remember_me]
current_user = user_from_cookie
else
current_user = User.find_by_id(session[:current_user_id])
end
What is wrong?
EDITED
Maybe we can solve this problem through APP1/config/routes.rb parameters (?!):
Example: in APP1/config/routes.rb
resources :users do
collection do
get 'current', :current_user => #current_user # ?!
end
end
or something like that.
When you do a request to another website, the cookies of the current user are not accessible. The request is from server to server, so the application knows nothing about the user requesting it. I think a solution would be to send the parameters yourself and check for those.
If APP2 requests something from the APP1 via ActiveResource it is not the same as the APP1 logged in user requesting it. Users cookies are not 'forwarded'. Somebody correct me if this is nonsense. :)
Authentication part here http://api.rubyonrails.org/classes/ActiveResource/Base.html has a couple of valid options listed.