require 'abstract_unit'

class SSLTest < ActionDispatch::IntegrationTest
  HEADERS = Rack::Utils::HeaderHash.new 'Content-Type' => 'text/html'

  attr_accessor :app

  def build_app(headers: {}, ssl_options: {})
    headers = HEADERS.merge(headers)
    ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options.reverse_merge(hsts: { subdomains: true })
  end
end

class RedirectSSLTest < SSLTest

  def assert_not_redirected(url, headers: {}, redirect: {}, deprecated_host: nil,
    deprecated_port: nil)

    self.app = build_app ssl_options: { redirect: redirect,
      host: deprecated_host, port: deprecated_port
    }

    get url, headers: headers
    assert_response :ok
  end

  def assert_redirected(redirect: {}, deprecated_host: nil, deprecated_port: nil,
    from: 'http://a/b?c=d', to: from.sub('http', 'https'))

    redirect = { status: 301, body: [] }.merge(redirect)

    self.app = build_app ssl_options: { redirect: redirect,
      host: deprecated_host, port: deprecated_port
    }

    get from
    assert_response redirect[:status] || 301
    assert_redirected_to to
    assert_equal redirect[:body].join, @response.body
  end

  def assert_post_redirected(redirect: {}, from: "http://a/b?c=d",
    to: from.sub("http", "https"))

    self.app = build_app ssl_options: { redirect: redirect }

    post from
    assert_response redirect[:status] || 307
    assert_redirected_to to
  end

  test 'exclude can avoid redirect' do
    excluding = { exclude: -> request { request.path =~ /healthcheck/ } }

    assert_not_redirected 'http://example.org/healthcheck', redirect: excluding
    assert_redirected from: 'http://example.org/', redirect: excluding
  end

  test 'https is not redirected' do
    assert_not_redirected 'https://example.org'
  end

  test 'proxied https is not redirected' do
    assert_not_redirected 'http://example.org', headers: { 'HTTP_X_FORWARDED_PROTO' => 'https' }
  end

  test 'http is redirected to https' do
    assert_redirected
  end

  test "http POST is redirected to https with status 307" do
    assert_post_redirected
  end

  test 'redirect with non-301 status' do
    assert_redirected redirect: { status: 307 }
  end

  test 'redirect with custom body' do
    assert_redirected redirect: { body: ['foo'] }
  end

  test 'redirect to specific host' do
    assert_redirected redirect: { host: 'ssl' }, to: 'https://ssl/b?c=d'
  end

  test 'redirect to default port' do
    assert_redirected redirect: { port: 443 }
  end

  test 'redirect to non-default port' do
    assert_redirected redirect: { port: 8443 }, to: 'https://a:8443/b?c=d'
  end

  test 'redirect to different host and non-default port' do
    assert_redirected redirect: { host: 'ssl', port: 8443 }, to: 'https://ssl:8443/b?c=d'
  end

  test 'redirect to different host including port' do
    assert_redirected redirect: { host: 'ssl:443' }, to: 'https://ssl:443/b?c=d'
  end

  test ':host is deprecated, moved within redirect: { host: … }' do
    assert_deprecated do
      assert_redirected deprecated_host: 'foo', to: 'https://foo/b?c=d'
    end
  end

  test ':port is deprecated, moved within redirect: { port: … }' do
    assert_deprecated do
      assert_redirected deprecated_port: 1, to: 'https://a:1/b?c=d'
    end
  end

  test 'no redirect with redirect set to false' do
    assert_not_redirected 'http://example.org', redirect: false
  end
end

class StrictTransportSecurityTest < SSLTest
  EXPECTED = 'max-age=15552000'
  EXPECTED_WITH_SUBDOMAINS = 'max-age=15552000; includeSubDomains'

  def assert_hsts(expected, url: 'https://example.org', hsts: { subdomains: true }, headers: {})
    self.app = build_app ssl_options: { hsts: hsts }, headers: headers
    get url
    assert_equal expected, response.headers['Strict-Transport-Security']
  end

  test 'enabled by default' do
    assert_hsts EXPECTED_WITH_SUBDOMAINS
  end

  test 'not sent with http:// responses' do
    assert_hsts nil, url: 'http://example.org'
  end

  test 'defers to app-provided header' do
    assert_hsts 'app-provided', headers: { 'Strict-Transport-Security' => 'app-provided' }
  end

  test 'hsts: true enables default settings' do
    assert_hsts EXPECTED, hsts: true
  end

  test 'hsts: false sets max-age to zero, clearing browser HSTS settings' do
    assert_hsts 'max-age=0', hsts: false
  end

  test ':expires sets max-age' do
    assert_deprecated do
      assert_hsts 'max-age=500', hsts: { expires: 500 }
    end
  end

  test ':expires supports AS::Duration arguments' do
    assert_deprecated do
      assert_hsts 'max-age=31557600', hsts: { expires: 1.year }
    end
  end

  test 'include subdomains' do
    assert_hsts "#{EXPECTED}; includeSubDomains", hsts: { subdomains: true }
  end

  test 'exclude subdomains' do
    assert_hsts EXPECTED, hsts: { subdomains: false }
  end

  test 'opt in to browser preload lists' do
    assert_deprecated do
      assert_hsts "#{EXPECTED}; preload", hsts: { preload: true }
    end
  end

  test 'opt out of browser preload lists' do
    assert_deprecated do
      assert_hsts EXPECTED, hsts: { preload: false }
    end
  end
end

class SecureCookiesTest < SSLTest
  DEFAULT = %(id=1; path=/\ntoken=abc; path=/; secure; HttpOnly)

  def get(**options)
    self.app = build_app(**options)
    super 'https://example.org'
  end

  def assert_cookies(*expected)
    assert_equal expected, response.headers['Set-Cookie'].split("\n")
  end

  def test_flag_cookies_as_secure
    get headers: { 'Set-Cookie' => DEFAULT }
    assert_cookies 'id=1; path=/; secure', 'token=abc; path=/; secure; HttpOnly'
  end

  def test_flag_cookies_as_secure_at_end_of_line
    get headers: { 'Set-Cookie' => 'problem=def; path=/; HttpOnly; secure' }
    assert_cookies 'problem=def; path=/; HttpOnly; secure'
  end

  def test_flag_cookies_as_secure_with_more_spaces_before
    get headers: { 'Set-Cookie' => 'problem=def; path=/; HttpOnly;  secure' }
    assert_cookies 'problem=def; path=/; HttpOnly;  secure'
  end

  def test_flag_cookies_as_secure_with_more_spaces_after
    get headers: { 'Set-Cookie' => 'problem=def; path=/; secure;  HttpOnly' }
    assert_cookies 'problem=def; path=/; secure;  HttpOnly'
  end

  def test_flag_cookies_as_secure_with_has_not_spaces_before
    get headers: { 'Set-Cookie' => 'problem=def; path=/;secure; HttpOnly' }
    assert_cookies 'problem=def; path=/;secure; HttpOnly'
  end

  def test_flag_cookies_as_secure_with_has_not_spaces_after
    get headers: { 'Set-Cookie' => 'problem=def; path=/; secure;HttpOnly' }
    assert_cookies 'problem=def; path=/; secure;HttpOnly'
  end

  def test_flag_cookies_as_secure_with_ignore_case
    get headers: { 'Set-Cookie' => 'problem=def; path=/; Secure; HttpOnly' }
    assert_cookies 'problem=def; path=/; Secure; HttpOnly'
  end

  def test_cookies_as_not_secure_with_secure_cookies_disabled
    get headers: { 'Set-Cookie' => DEFAULT }, ssl_options: { secure_cookies: false }
    assert_cookies(*DEFAULT.split("\n"))
  end

  def test_no_cookies
    get
    assert_nil response.headers['Set-Cookie']
  end

  def test_keeps_original_headers_behavior
    get headers: { 'Connection' => %w[close] }
    assert_equal 'close', response.headers['Connection']
  end
end
