require 'spec_helper' describe Clearance::Session do before { Timecop.freeze } after { Timecop.return } let(:headers) { {} } let(:session) { Clearance::Session.new(env_without_remember_token) } let(:user) { create(:user) } it 'finds a user from a cookie' do user = create(:user) env = env_with_remember_token(user.remember_token) session = Clearance::Session.new(env) expect(session).to be_signed_in expect(session.current_user).to eq user end it 'returns nil for an unknown user' do env = env_with_remember_token('bogus') session = Clearance::Session.new(env) expect(session).to be_signed_out expect(session.current_user).to be_nil end it 'returns nil without a remember token' do expect(session).to be_signed_out expect(session.current_user).to be_nil end context "with a custom cookie name" do it "sets a custom cookie name in the header" do Clearance.configuration.cookie_name = "custom_cookie_name" session.sign_in user session.add_cookie_to_headers(headers) expect(headers["Set-Cookie"]).to match(/custom_cookie_name=.+;/) end end describe '#sign_in' do it 'sets current_user' do user = build(:user) session.sign_in user expect(session.current_user).to eq user end context 'with a block' do it 'passes the success status to the block when sign in succeeds' do success_status = stub_status(Clearance::SuccessStatus, true) success_lambda = stub_callable session.sign_in build(:user), &success_lambda expect(success_lambda).to have_been_called.with(success_status) end it 'passes the failure status to the block when sign in fails' do failure_status = stub_status(Clearance::FailureStatus, false) failure_lambda = stub_callable session.sign_in nil, &failure_lambda expect(failure_lambda).to have_been_called.with(failure_status) end def stub_status(status_class, success) double("status", success?: success).tap do |status| allow(status_class).to receive(:new).and_return(status) end end def stub_callable lambda {}.tap do |callable| allow(callable).to receive(:call) end end end context 'with nil argument' do it 'assigns current_user' do session.sign_in nil expect(session.current_user).to be_nil end end context 'with a sign in stack' do it 'runs the first guard' do guard = stub_sign_in_guard(succeed: true) user = build(:user) session.sign_in user expect(guard).to have_received(:call) end it 'will not sign in the user if the guard stack fails' do stub_sign_in_guard(succeed: false) user = build(:user) session.sign_in user expect(session.instance_variable_get("@cookies")).to be_nil expect(session.current_user).to be_nil end def stub_sign_in_guard(options) session_status = stub_status(options.fetch(:succeed)) double("guard", call: session_status).tap do |guard| Clearance.configuration.sign_in_guards << stub_guard_class(guard) end end def stub_default_sign_in_guard double("default_sign_in_guard").tap do |sign_in_guard| allow(Clearance::DefaultSignInGuard).to receive(:new). with(session). and_return(sign_in_guard) end end def stub_guard_class(guard) double("guard_class").tap do |guard_class| allow(guard_class).to receive(:to_s). and_return(guard_class) allow(guard_class).to receive(:constantize). and_return(guard_class) allow(guard_class).to receive(:new). with(session, stub_default_sign_in_guard). and_return(guard) end end def stub_status(success) double("status", success?: success) end after do Clearance.configuration.sign_in_guards = [] end end end context 'if httponly is set' do before do session.sign_in(user) end it 'sets a httponly cookie' do session.add_cookie_to_headers(headers) expect(headers['Set-Cookie']).to match(/remember_token=.+; HttpOnly/) end end context 'if httponly is not set' do before do Clearance.configuration.httponly = false session.sign_in(user) end it 'sets a standard cookie' do session.add_cookie_to_headers(headers) expect(headers['Set-Cookie']).not_to match(/remember_token=.+; HttpOnly/) end end context "if same_site is set" do before do Clearance.configuration.same_site = :lax session.sign_in(user) end it "sets a same-site cookie" do session.add_cookie_to_headers(headers) expect(headers["Set-Cookie"]).to match(/remember_token=.+; SameSite/) end end context "if same_site is not set" do before do session.sign_in(user) end it "sets a standard cookie" do session.add_cookie_to_headers(headers) expect(headers["Set-Cookie"]).to_not match(/remember_token=.+; SameSite/) end end describe 'remember token cookie expiration' do context 'default configuration' do it 'is set to 1 year from now' do user = double("User", remember_token: "123abc") headers = {} session = Clearance::Session.new(env_without_remember_token) session.sign_in user session.add_cookie_to_headers headers expect(headers).to set_cookie( 'remember_token', user.remember_token, 1.year.from_now ) end end context 'configured with lambda taking one argument' do it 'it can use other cookies to set the value of the expires token' do remembered_expires = 12.hours.from_now expires_at = ->(cookies) do cookies['remember_me'] ? remembered_expires : nil end with_custom_expiration expires_at do user = double("User", remember_token: "123abc") headers = {} environment = env_with_cookies(remember_me: 'true') session = Clearance::Session.new(environment) session.sign_in user session.add_cookie_to_headers headers expect(headers).to set_cookie( 'remember_token', user.remember_token, remembered_expires ) end end end end describe 'secure cookie option' do context 'when not set' do before do session.sign_in(user) end it 'sets a standard cookie' do session.add_cookie_to_headers(headers) expect(headers['Set-Cookie']).not_to match(/remember_token=.+; secure/) end end context 'when set' do before do Clearance.configuration.secure_cookie = true session.sign_in(user) end it 'sets a secure cookie' do session.add_cookie_to_headers(headers) expect(headers['Set-Cookie']).to match(/remember_token=.+; secure/) end end end describe "cookie domain option" do context "when set" do before do Clearance.configuration.cookie_domain = cookie_domain session.sign_in(user) end context "with string" do let(:cookie_domain) { ".example.com" } it "sets a standard cookie" do session.add_cookie_to_headers(headers) expect(headers['Set-Cookie']).to match(/domain=\.example\.com; path/) end end context "with lambda" do let(:cookie_domain) { lambda { |_r| ".example.com" } } it "sets a standard cookie" do session.add_cookie_to_headers(headers) expect(headers['Set-Cookie']).to match(/domain=\.example\.com; path/) end end end context 'when not set' do before { session.sign_in(user) } it 'sets a standard cookie' do session.add_cookie_to_headers(headers) expect(headers["Set-Cookie"]).not_to match(/domain=.+; path/) end end end describe 'cookie path option' do context 'when not set' do before { session.sign_in(user) } it 'sets a standard cookie' do session.add_cookie_to_headers(headers) expect(headers["Set-Cookie"]).to_not match(/domain=.+; path/) end end context 'when set' do before do Clearance.configuration.cookie_path = '/user' session.sign_in(user) end it 'sets a standard cookie' do session.add_cookie_to_headers(headers) expect(headers['Set-Cookie']).to match(/path=\/user; expires/) end end end it 'does not set a remember token when signed out' do headers = {} session = Clearance::Session.new(env_without_remember_token) session.add_cookie_to_headers headers expect(headers["Set-Cookie"]).to be nil end it 'signs out a user' do user = create(:user) old_remember_token = user.remember_token env = env_with_remember_token(old_remember_token) session = Clearance::Session.new(env) session.sign_out expect(session.current_user).to be_nil expect(user.reload.remember_token).not_to eq old_remember_token end def env_with_cookies(cookies) Rack::MockRequest.env_for '/', 'HTTP_COOKIE' => serialize_cookies(cookies) end def env_with_remember_token(token) env_with_cookies 'remember_token' => token end def env_without_remember_token env_with_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 have_been_called have_received(:call) end def with_custom_expiration(custom_duration) Clearance.configuration.cookie_expiration = custom_duration yield end end