diff --git a/Gemfile.lock b/Gemfile.lock index 91df0ecfb..6ab15d4f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,6 +54,8 @@ GEM rspec (>= 2.6.0) bcat (0.6.1) rack (~> 1.0) + bourne (1.0) + mocha (= 0.9.8) builder (2.1.2) capybara (1.0.1) mime-types (>= 1.16) @@ -99,7 +101,8 @@ GEM mime-types (~> 1.16) treetop (~> 1.4.8) mime-types (1.16) - mocha (0.9.12) + mocha (0.9.8) + rake nokogiri (1.5.0) polyglot (0.3.2) rack (1.2.3) @@ -147,6 +150,7 @@ GEM sqlite3 (1.3.4) term-ansicolor (1.0.6) thor (0.14.6) + timecop (0.3.5) treetop (1.4.10) polyglot polyglot (>= 0.3.1) @@ -160,6 +164,7 @@ PLATFORMS DEPENDENCIES appraisal (~> 0.3.8) aruba (~> 0.4.2) + bourne bundler (~> 1.0.0) capybara (~> 1.0.0) clearance! @@ -167,7 +172,7 @@ DEPENDENCIES database_cleaner factory_girl_rails launchy - mocha rspec-rails (~> 2.6.0) shoulda-matchers! sqlite3 + timecop diff --git a/README.md b/README.md index 2817427bf..5b8e846fc 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,26 @@ Clearance will deliver one email on your app's behalf: when a user resets their config.mailer_sender = "me@example.com" end +Rack +---- + +Clearance adds its session to the Rack environment hash so middleware and other +Rack applications can interact with it: + + class Bubblegum::Middleware + def initialize(app) + @app = app + end + + def call(env) + if env[:clearance].signed_in? + env[:clearance].current_user.bubble_gum + end + @app.call(env) + end + end + + Overriding defaults ------------------- @@ -214,16 +234,18 @@ Then run your tests! rake -Optional test matchers ----------------------- +Testing +------- -Clearance comes with test matchers that are compatible with RSpec and Test::Unit. +If you want to write Rails functional tests or controller specs with Clearance, +you'll need to require the included test helpers and matchers. -To use them, require the test matchers. For example, in spec/support/clearance.rb: +For example, in spec/support/clearance.rb or test/test_helper.rb: require 'clearance/testing' -You'll then have access to methods like: +This will make Clearance::Authentication methods work in your controllers +during functional tests and provide access to helper methods like: sign_in sign_in_as(user) diff --git a/clearance.gemspec b/clearance.gemspec index ce472ef2d..b3c5c49c1 100644 --- a/clearance.gemspec +++ b/clearance.gemspec @@ -30,7 +30,8 @@ Gem::Specification.new do |s| s.add_development_dependency('cucumber-rails', '~> 1.0.2') s.add_development_dependency('rspec-rails', '~> 2.6.0') s.add_development_dependency('sqlite3') - s.add_development_dependency('mocha') + s.add_development_dependency('bourne') + s.add_development_dependency('timecop') if s.respond_to? :specification_version then current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION diff --git a/features/support/env.rb b/features/support/env.rb index 4f6305794..198083a69 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -53,4 +53,3 @@ rescue NameError raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." end - diff --git a/gemfiles/3.0.9.gemfile.lock b/gemfiles/3.0.9.gemfile.lock index 11c94df14..7ca380b7a 100644 --- a/gemfiles/3.0.9.gemfile.lock +++ b/gemfiles/3.0.9.gemfile.lock @@ -5,9 +5,9 @@ GIT shoulda-matchers (1.0.0.beta3) PATH - remote: /Users/croaky/dev/clearance + remote: /Users/jferris/Source/clearance specs: - clearance (0.12.0) + clearance (0.13.0) diesel (~> 0.1.5) rails (>= 3.0) diff --git a/gemfiles/3.1.0.gemfile.lock b/gemfiles/3.1.0.gemfile.lock index 97ff6f869..5d0aa3d5f 100644 --- a/gemfiles/3.1.0.gemfile.lock +++ b/gemfiles/3.1.0.gemfile.lock @@ -5,9 +5,9 @@ GIT shoulda-matchers (1.0.0.beta3) PATH - remote: /Users/croaky/dev/clearance + remote: /Users/jferris/Source/clearance specs: - clearance (0.12.0) + clearance (0.13.0) diesel (~> 0.1.5) rails (>= 3.0) @@ -154,7 +154,7 @@ GEM sprockets (2.0.0) hike (~> 1.2) rack (~> 1.0) - tilt (!= 1.3.0, ~> 1.1) + tilt (~> 1.1, != 1.3.0) spruz (0.2.13) sqlite3 (1.3.4) term-ansicolor (1.0.6) diff --git a/lib/clearance.rb b/lib/clearance.rb index e353fead1..f4382e3fa 100644 --- a/lib/clearance.rb +++ b/lib/clearance.rb @@ -1,5 +1,7 @@ require 'clearance/configuration' +require 'clearance/session' +require 'clearance/rack_session' require 'clearance/authentication' require 'clearance/user' require 'clearance/engine' -require 'clearance/password_strategies' \ No newline at end of file +require 'clearance/password_strategies' diff --git a/lib/clearance/authentication.rb b/lib/clearance/authentication.rb index 5afe6c5db..0a9607e24 100644 --- a/lib/clearance/authentication.rb +++ b/lib/clearance/authentication.rb @@ -10,32 +10,32 @@ module Authentication :authorize, :deny_access end - # User in the current cookie + # Finds the user from the rack clearance session # # @return [User, nil] def current_user - @_current_user ||= user_from_cookie + clearance_session.current_user end # Set the current user # # @param [User] def current_user=(user) - @_current_user = user + clearance_session.sign_in user end # Is the current user signed in? # # @return [true, false] def signed_in? - ! current_user.nil? + clearance_session.signed_in? end # Is the current user signed out? # # @return [true, false] def signed_out? - current_user.nil? + !signed_in? end # Sign user in to cookie. @@ -45,13 +45,7 @@ def signed_out? # @example # sign_in(@user) def sign_in(user) - if user - cookies[:remember_token] = { - :value => user.remember_token, - :expires => Clearance.configuration.cookie_expiration.call - } - self.current_user = user - end + clearance_session.sign_in user end # Sign user out of cookie. @@ -59,9 +53,7 @@ def sign_in(user) # @example # sign_out def sign_out - current_user.reset_remember_token! if current_user - cookies.delete(:remember_token) - self.current_user = nil + clearance_session.sign_out end # Find the user by the given params or return nil. @@ -107,10 +99,8 @@ def handle_unverified_request protected - def user_from_cookie - if token = cookies[:remember_token] - ::User.find_by_remember_token(token) - end + def clearance_session + request.env[:clearance] end def store_location diff --git a/lib/clearance/engine.rb b/lib/clearance/engine.rb index aa542a866..0bd0185ef 100644 --- a/lib/clearance/engine.rb +++ b/lib/clearance/engine.rb @@ -6,5 +6,7 @@ class Engine < Rails::Engine initializer "clearance.filter" do |app| app.config.filter_parameters += [:token, :password] end + + config.app_middleware.insert_after ActionDispatch::Cookies, Clearance::RackSession end end diff --git a/lib/clearance/rack_session.rb b/lib/clearance/rack_session.rb new file mode 100644 index 000000000..fff233b19 --- /dev/null +++ b/lib/clearance/rack_session.rb @@ -0,0 +1,15 @@ +module Clearance + class RackSession + def initialize(app) + @app = app + end + + def call(env) + session = Clearance::Session.new(env) + env_with_clearance = env.merge(:clearance => session) + response = @app.call(env_with_clearance) + session.add_cookie_to_headers response[1] + response + end + end +end diff --git a/lib/clearance/session.rb b/lib/clearance/session.rb new file mode 100644 index 000000000..769ba47b6 --- /dev/null +++ b/lib/clearance/session.rb @@ -0,0 +1,55 @@ +module Clearance + class Session + REMEMBER_TOKEN_COOKIE = "remember_token".freeze + + def initialize(env) + @env = env + end + + def signed_in? + current_user.present? + end + + def current_user + @current_user ||= with_remember_token do |token| + ::User.find_by_remember_token(token) + end + end + + def sign_in(user) + @current_user = user + end + + def sign_out + current_user.reset_remember_token! if signed_in? + @current_user = nil + cookies.delete(REMEMBER_TOKEN_COOKIE) + end + + def add_cookie_to_headers(headers) + if signed_in? + Rack::Utils.set_cookie_header!(headers, + REMEMBER_TOKEN_COOKIE, + :value => current_user.remember_token, + :expires => Clearance.configuration.cookie_expiration.call, + :path => "/") + end + end + + private + + def with_remember_token + if token = remember_token + yield token + end + end + + def remember_token + cookies[REMEMBER_TOKEN_COOKIE] + end + + def cookies + @cookies ||= Rack::Request.new(@env).cookies + end + end +end diff --git a/lib/clearance/testing/helpers.rb b/lib/clearance/testing/helpers.rb index c9808d461..d756a842f 100644 --- a/lib/clearance/testing/helpers.rb +++ b/lib/clearance/testing/helpers.rb @@ -13,6 +13,11 @@ def sign_in def sign_out @controller.current_user = nil end + + def setup_controller_request_and_response + super + @request.env[:clearance] = Clearance::Session.new(@request.env) + end end end end diff --git a/spec/clearance/rack_session_spec.rb b/spec/clearance/rack_session_spec.rb new file mode 100644 index 000000000..d37ca3429 --- /dev/null +++ b/spec/clearance/rack_session_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Clearance::RackSession do + it "injects a clearance session into the environment" do + expected_session = "the session" + expected_session.stubs(:add_cookie_to_headers) + Clearance::Session.stubs(:new => expected_session) + headers = { "X-Roaring-Lobster" => "Red" } + + app = Rack::Builder.new do + use Clearance::RackSession + run lambda { |env| Rack::Response.new(env[:clearance], 200, headers).finish } + end + + env = Rack::MockRequest.env_for("/") + + response = Rack::MockResponse.new(*app.call(env)) + + Clearance::Session.should have_received(:new).with(env) + response.body.should == expected_session + expected_session.should have_received(:add_cookie_to_headers).with(has_entries(headers)) + end +end diff --git a/spec/clearance/session_spec.rb b/spec/clearance/session_spec.rb new file mode 100644 index 000000000..b09dc0d25 --- /dev/null +++ b/spec/clearance/session_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe Clearance::Session do + before { Timecop.freeze } + after { Timecop.return } + + it "finds a user from a cookie" do + user = Factory(:user) + env = env_with_remember_token(user.remember_token) + + session = Clearance::Session.new(env) + session.should be_signed_in + session.current_user.should == user + end + + it "returns nil for an unknown user" do + user = Factory(:user) + env = env_with_remember_token("bogus") + + session = Clearance::Session.new(env) + session.should_not be_signed_in + session.current_user.should be_nil + end + + it "returns nil without a remember token" do + env = env_without_remember_token + session = Clearance::Session.new(env) + session.should_not be_signed_in + session.current_user.should be_nil + end + + it "signs in a given user" do + user = Factory(:user) + session = Clearance::Session.new(env_without_remember_token) + session.sign_in user + session.current_user.should == user + end + + it "sets a remember token cookie with a default expiration of 1 year from now" do + user = Factory(:user) + headers = {} + session = Clearance::Session.new(env_without_remember_token) + session.sign_in user + session.add_cookie_to_headers headers + headers.should set_cookie("remember_token", user.remember_token, 1.year.from_now) + end + + it "sets a remember token cookie with a custom expiration" do + custom_expiration = 1.day.from_now + with_custom_expiration 1.day.from_now do + user = Factory(:user) + headers = {} + session = Clearance::Session.new(env_without_remember_token) + session.sign_in user + session.add_cookie_to_headers headers + headers.should set_cookie("remember_token", user.remember_token, 1.day.from_now) + Clearance.configuration.cookie_expiration.call.should be_within(100).of(1.year.from_now) + end + end + + it "doesn't set a remember token when signed out" do + headers = {} + session = Clearance::Session.new(env_without_remember_token) + session.add_cookie_to_headers headers + headers.should_not set_cookie("remember_token") + end + + it "signs out a user" do + user = Factory(:user) + old_remember_token = user.remember_token + env = env_with_remember_token(old_remember_token) + + session = Clearance::Session.new(env) + session.sign_out + session.current_user.should be_nil + user.reload.remember_token.should_not == old_remember_token + end + + def env_with_remember_token(token) + env_with_cookies("remember_token" => token) + end + + def env_without_remember_token + env_with_cookies({}) + end + + def env_with_cookies(cookies) + Rack::MockRequest.env_for("/", "HTTP_COOKIE" => serialize_cookies(cookies)) + end + + def serialize_cookies(hash) + header = {} + hash.each do |key, value| + Rack::Utils.set_cookie_header!(header, key, value) + end + header['Set-Cookie'] + end + + def with_custom_expiration(custom_duration) + Clearance.configuration.cookie_expiration = lambda { custom_duration } + ensure + restore_default_config + end + + def restore_default_config + Clearance.configuration = nil + Clearance.configure {} + end +end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index e1079396d..33ced2496 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -20,12 +20,8 @@ it { should redirect_to_url_after_create } - it "sets a remember token cookie" do - should set_cookie("remember_token", "old-token", Clearance.configuration.cookie_expiration.call) - end - - it "should have a default of 1 year from now" do - Clearance.configuration.cookie_expiration.call.should be_within(100).of(1.year.from_now) + it "sets the user in the clearance session" do + controller.current_user.should == @user end it "should not change the remember token" do @@ -33,50 +29,6 @@ end end - describe "on POST to #create with good credentials - cookie duration set to 2 weeks" do - custom_duration = 2.weeks.from_now.utc - - before do - Clearance.configuration.cookie_expiration = lambda { custom_duration } - @user = Factory(:user) - @user.update_attribute(:remember_token, "old-token2") - post :create, :session => { - :email => @user.email, - :password => @user.password } - end - - it "sets a remember token cookie" do - should set_cookie("remember_token", "old-token2", custom_duration) - end - - after do - # restore default Clearance configuration - Clearance.configuration = nil - Clearance.configure {} - end - end - - describe "on POST to #create with good credentials - cookie expiration set to nil (session cookie)" do - before do - Clearance.configuration.cookie_expiration = lambda { nil } - @user = Factory(:user) - @user.update_attribute(:remember_token, "old-token3") - post :create, :session => { - :email => @user.email, - :password => @user.password } - end - - it "unsets a remember token cookie" do - should set_cookie("remember_token", "old-token3", nil) - end - - after do - # restore default Clearance configuration - Clearance.configuration = nil - Clearance.configure {} - end - end - describe "on POST to #create with good credentials and a session return url" do before do @user = Factory(:user) @@ -141,10 +93,6 @@ it { should redirect_to_url_after_destroy } - it "should delete the cookie token" do - cookies['remember_token'].should be_nil - end - it "should reset the remember token" do @user.reload.remember_token.should_not == "old-token" end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4e75f833f..f6e451924 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,6 +10,8 @@ require 'diesel/testing' require 'rspec/rails' +require 'bourne' +require 'timecop' require 'clearance/testing' diff --git a/spec/support/cookies.rb b/spec/support/cookies.rb index aa27a904f..4049de0c2 100644 --- a/spec/support/cookies.rb +++ b/spec/support/cookies.rb @@ -1,27 +1,29 @@ -RSpec::Matchers.define :set_cookie do |name, value, expected_expires_at| +RSpec::Matchers.define :set_cookie do |name, expected_value, expected_expires_at| match do |subject| - @response = subject.response - @name = name - @value = value + @headers = subject + @expected_name = name + @expected_value = expected_value @expected_expires_at = expected_expires_at extract_cookies find_expected_cookie parse_expiration + parse_value + parse_path ensure_cookie_set - ensure_value_correct ensure_expiration_correct + ensure_path_is_correct end def extract_cookies - @cookie_headers = @response.headers['Set-Cookie'] || [] + @cookie_headers = @headers['Set-Cookie'] || [] @cookie_headers = [@cookie_headers] if @cookie_headers.respond_to?(:to_str) end def find_expected_cookie @cookie = @cookie_headers.detect do |header| - header =~ /^#{@name}=[^;]*(;|$)/ + header =~ /^#{@expected_name}=[^;]*(;|$)/ end end @@ -31,21 +33,29 @@ def parse_expiration end end - def ensure_cookie_set - @cookie.should_not be_nil + def parse_value + if @cookie && result = @cookie.match(/=(.*?)(?:;|$)/) + @value = result[1] + end + end + + def parse_path + if @cookie && result = @cookie.match(/; path=(.*?)(;|$)/) + @path = result[1] + end end - def ensure_value_correct - @response.cookies[@name].should == @value + def ensure_cookie_set + @value.should == @expected_value end def ensure_expiration_correct - if @expected_expires_at - @expires_at.should_not be_nil - @expires_at.should be_within(100).of(@expected_expires_at) - else - @expires_at.should be_nil - end + @expires_at.should_not be_nil + @expires_at.should be_within(100).of(@expected_expires_at) + end + + def ensure_path_is_correct + @path.should == "/" end failure_message do @@ -53,20 +63,14 @@ def ensure_expiration_correct end def expectation - base = "Expected a cookie named #{@name} with value #{@value.inspect} " - if @expected_expires_at - base << "expiring at #{@expected_expires_at.inspect}" - else - base << "with no expiration" - end - base + "a cookie named #{@expected_name} with value #{@expected_value.inspect} expiring at #{@expected_expires_at.inspect}" end def result if @cookie - "value #{@value.inspect} expiring #{@expires_at.inspect}" + @cookie else - "cookies #{@response.cookies.inspect}" + @cookie_headers.join("; ") end end end