require 'test_helper'

return unless Gem::Version.new(ActiveSupport.version) >= Gem::Version.new('7.2')

class RedisStoreIntegrationTest < ::ActionDispatch::IntegrationTest
  SessionKey = '_session_id'
  SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'

  test "reads the data" do
    with_test_route_set do
      get '/set_session_value'
      assert_response :success
      assert cookies['_session_id'].present?

      get '/get_session_value'
      assert_response :success
      assert_equal response.body, 'foo: "bar"'
    end
  end

  test "should get nil session value" do
    with_test_route_set do
      get '/get_session_value'
      assert_response :success
      assert_equal 'foo: nil', response.body
    end
  end

  test "should delete the data after session reset" do
    with_test_route_set do
      get '/set_session_value'
      assert_response :success
      assert cookies['_session_id'].present?
      if cookies.respond_to?(:get_cookie)
        session_cookie = cookies.get_cookie('_session_id')
      else
        session_cookie = cookies.send(:hash_for)['_session_id']
      end

      get '/call_reset_session'
      assert_response :success
      assert !headers['Set-Cookie'].blank?

      cookies << session_cookie

      get '/get_session_value'
      assert_response :success
      assert_equal 'foo: nil', response.body
    end
  end

  test "should set a non-secure cookie by default" do
    with_test_route_set do
      https!

      get '/set_session_value'
      assert_response :success

      cookie = cookies.instance_variable_get('@cookies').first

      assert !cookie.secure?
    end
  end

  test "should set a secure cookie when the 'secure' option is set" do
    cookie_options(secure: true)

    with_test_route_set do
      https!

      get '/set_session_value'
      assert_response :success

      cookie = cookies.instance_variable_get('@cookies').first

      assert cookie.secure?
    end
  end

  test "should set a signed cookie when the 'signed' option is set" do
    cookie_options(signed: true)

    with_test_route_set do
      https!

      get '/set_session_value'
      assert_response :success

      cookie = cookies.instance_variable_get('@cookies').first

      assert_includes cookie.raw, '_session_id='
    end
  end


  test "should set a http-only cookie by default" do
    with_test_route_set do
      get '/set_session_value'
      assert_response :success

      cookie = cookies.instance_variable_get('@cookies').first
      options = cookie.instance_variable_get('@options')

      if Gem::Version.new(ActiveSupport.version) < Gem::Version.new('7.1')
        assert options.key?('HttpOnly')
      else
        assert options.key?('httponly')
      end
    end
  end

  test "should set a non-http-only cookie when the 'httponlty' option is set to false" do
    cookie_options(httponly: false)

    with_test_route_set do
      get '/set_session_value'
      assert_response :success

      cookie = cookies.instance_variable_get('@cookies').first
      options = cookie.instance_variable_get('@options')

      if Gem::Version.new(ActiveSupport.version) < Gem::Version.new('7.1')
        assert !options.key?('HttpOnly')
      else
        assert !options.key?('httponly')
      end
    end
  end

  test "should not send cookies on write, not read" do
    with_test_route_set do
      get '/get_session_value'
      assert_response :success
      assert_equal 'foo: nil', response.body
      assert cookies['_session_id'].nil?
    end
  end

  test "should set session value after session reset" do
    with_test_route_set do
      get '/set_session_value'
      assert_response :success
      assert cookies['_session_id'].present?
      session_id = cookies['_session_id']

      get '/call_reset_session'
      assert_response :success
      assert !headers['Set-Cookie'].blank?

      get '/get_session_value'
      assert_response :success
      assert_equal 'foo: nil', response.body

      get '/get_session_id'
      assert_response :success
      assert(response.body != session_id)
    end
  end

  test "should be able to read session id without accessing the session hash" do
    with_test_route_set do
      get '/set_session_value'
      assert_response :success
      assert cookies['_session_id'].present?
      session_id = cookies['_session_id']

      get '/get_session_id'
      assert_response :success
      assert_equal response.body, session_id
    end
  end

  test "should auto-load unloaded class" do
    with_test_route_set do
      with_autoload_path "session_autoload_test" do
        get '/set_serialized_session_value'
        assert_response :success
        assert cookies['_session_id'].present?
      end

      with_autoload_path "session_autoload_test" do
        get '/get_session_id'
        assert_response :success
      end

      with_autoload_path "session_autoload_test" do
        get '/get_session_value'
        assert_response :success
        assert_equal response.body, 'foo: #<SessionAutoloadTest::Foo bar:"baz">'
      end
    end
  end

  test "should not resend the cookie again if session_id cookie is already exists" do
    with_test_route_set do
      get '/set_session_value'
      assert_response :success
      assert cookies['_session_id'].present?

      get '/get_session_value'
      assert_response :success
      assert headers['Set-Cookie'].nil?
    end
  end

  test "should prevent session fixation" do
    with_test_route_set do
      get '/get_session_value'
      assert_response :success
      assert_equal 'foo: nil', response.body
      session_id = cookies['_session_id']

      reset!

      get '/set_session_value', headers: { _session_id: session_id }
      assert_response :success
      assert(cookies['_session_id'] != session_id)
    end
  end

  test "should write the data with expiration time" do
    with_test_route_set do
      get '/set_session_value_with_expiry'
      assert_response :success

      get '/get_session_value'
      assert_response :success
      assert_equal response.body, 'foo: "bar"'

      sleep 1

      get '/get_session_value'
      assert_response :success
      assert_equal 'foo: nil', response.body
    end
  end

  test "session store with explicit domain" do
    cookie_options(:domain => "example.es")

    with_test_route_set do
      get '/set_session_value'
      assert_match(/domain=example\.es/, headers['Set-Cookie'])
      headers['Set-Cookie']
    end
  end

  test "session store without domain" do
    with_test_route_set do
      get '/set_session_value'
      assert_no_match(/domain\=/, headers['Set-Cookie'])
    end
  end

  test "session store with nil domain" do
    cookie_options(:domain => nil)

    with_test_route_set do
      get '/set_session_value'
      assert_no_match(/domain\=/, headers['Set-Cookie'])
    end
  end

  test "session store with all domains" do
    cookie_options(:domain => :all)

    with_test_route_set do
      get '/set_session_value'
      assert_match(/domain=\.?example\.com/, headers['Set-Cookie'])
    end
  end

  private

    def cookie_options(options = {})
      (@cookie_options ||= { key: SessionKey }).merge!(options)
    end

    def app
      @app ||= self.class.build_app do |middleware|
        middleware.use ActionDispatch::Session::RedisStore, cookie_options
      end
    end

    # from actionpack/test/abstract_unit.rb
    class RoutedRackApp
      class Config < Struct.new(:middleware)
      end

      attr_reader :routes

      def initialize(routes, &blk)
        @routes = routes
        @stack = ActionDispatch::MiddlewareStack.new(&blk)
        @app = @stack.build(@routes)
        @secret = SecureRandom.hex
        @key_generator = ActiveSupport::CachingKeyGenerator.new(
          ActiveSupport::KeyGenerator.new(@secret, iterations: 2)
        )
      end
      def call(env)
        env[ActionDispatch::Cookies::GENERATOR_KEY] = @key_generator
        env[ActionDispatch::Cookies::SIGNED_COOKIE_SALT] = SecureRandom.hex
        if defined? ActionDispatch::Cookies::COOKIES_ROTATIONS
          env[ActionDispatch::Cookies::COOKIES_ROTATIONS] = ActiveSupport::Messages::RotationConfiguration.new
        end
        @app.call(env)
      end
      def config
        Config.new(@stack)
      end
    end

    # from actionpack/test/abstract_unit.rb
    def self.build_app(routes = nil)
      RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
        middleware.use ActionDispatch::DebugExceptions
        middleware.use ActionDispatch::Callbacks
        # middleware.use ActionDispatch::ParamsParser
        middleware.use ActionDispatch::Cookies
        middleware.use ActionDispatch::Flash
        middleware.use Rack::Head
        yield(middleware) if block_given?
      end
    end

    # from actionpack/test/dispatch/session/cookie_store_test.rb
    def with_test_route_set
      with_routing do |set|
        set.draw do
          get :no_session_access,             to: 'test#no_session_access'
          get :set_session_value,             to: 'test#set_session_value'
          get :set_session_value_with_expiry, to: 'test#set_session_value_with_expiry'
          get :set_serialized_session_value,  to: 'test#set_serialized_session_value'
          get :get_session_value,             to: 'test#get_session_value'
          get :get_session_id,                to: 'test#get_session_id'
          get :call_reset_session,            to: 'test#call_reset_session'
        end
        yield
      end
    end
end
