# frozen_string_literal: true
require_relative "helper"

unless Gem::HAVE_OPENSSL
  warn "Skipping Gem::Security::Signer tests.  openssl not found."
end

class TestGemSecuritySigner < Gem::TestCase
  ALTERNATE_KEY  = load_key "alternate"
  CHILD_KEY      = load_key "child"
  GRANDCHILD_KEY = load_key "grandchild"

  CHILD_CERT      = load_cert "child"
  GRANDCHILD_CERT = load_cert "grandchild"
  EXPIRED_CERT    = load_cert "expired"

  def setup
    super

    @cert_file = PUBLIC_CERT
  end

  def test_initialize
    signer = Gem::Security::Signer.new nil, nil

    assert_nil signer.key
    assert_nil signer.cert_chain
  end

  def test_initialize_cert_chain_empty
    signer = Gem::Security::Signer.new PUBLIC_KEY, []

    assert_empty signer.cert_chain
  end

  def test_initialize_cert_chain_mixed
    signer = Gem::Security::Signer.new nil, [@cert_file, CHILD_CERT]

    assert_equal [PUBLIC_CERT, CHILD_CERT].map {|c| c.to_pem },
                 signer.cert_chain.map {|c| c.to_pem }
  end

  def test_initialize_cert_chain_invalid
    assert_raise OpenSSL::X509::CertificateError do
      Gem::Security::Signer.new nil, ["garbage"]
    end
  end

  def test_initialize_cert_chain_path
    signer = Gem::Security::Signer.new nil, [@cert_file]

    assert_equal [PUBLIC_CERT].map {|c| c.to_pem },
                 signer.cert_chain.map {|c| c.to_pem }
  end

  def test_initialize_default
    FileUtils.mkdir_p File.join(Gem.user_home, ".gem")

    private_key_path = File.join Gem.user_home, ".gem", "gem-private_key.pem"
    Gem::Security.write PRIVATE_KEY, private_key_path

    public_cert_path = File.join Gem.user_home, ".gem", "gem-public_cert.pem"
    Gem::Security.write PUBLIC_CERT, public_cert_path

    signer = Gem::Security::Signer.new nil, nil

    assert_equal PRIVATE_KEY.to_pem, signer.key.to_pem
    assert_equal [PUBLIC_CERT.to_pem], signer.cert_chain.map {|c| c.to_pem }
  end

  def test_initialize_key_path
    key_file = PRIVATE_KEY_PATH

    signer = Gem::Security::Signer.new key_file, nil

    assert_equal PRIVATE_KEY.to_s, signer.key.to_s
  end

  def test_initialize_encrypted_key_path
    key_file = ENCRYPTED_PRIVATE_KEY_PATH

    signer = Gem::Security::Signer.new key_file, nil, PRIVATE_KEY_PASSPHRASE

    assert_equal ENCRYPTED_PRIVATE_KEY.to_s, signer.key.to_s
  end

  def test_extract_name
    signer = Gem::Security::Signer.new nil, nil

    assert_equal "child@example", signer.extract_name(CHILD_CERT)
  end

  def test_load_cert_chain
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    signer = Gem::Security::Signer.new nil, []
    signer.cert_chain.replace [CHILD_CERT]

    signer.load_cert_chain

    assert_equal [PUBLIC_CERT.to_pem, CHILD_CERT.to_pem],
                 signer.cert_chain.map {|c| c.to_pem }
  end

  def test_load_cert_chain_broken
    Gem::Security.trust_dir.trust_cert CHILD_CERT

    signer = Gem::Security::Signer.new nil, []
    signer.cert_chain.replace [GRANDCHILD_CERT]

    signer.load_cert_chain

    assert_equal [CHILD_CERT.to_pem, GRANDCHILD_CERT.to_pem],
                 signer.cert_chain.map {|c| c.to_pem }
  end

  def test_sign
    signer = Gem::Security::Signer.new PRIVATE_KEY, [PUBLIC_CERT]

    signature = signer.sign "hello"

    expected = <<-EXPECTED
FmrCYxEXW3dgYYNMxPdS16VrdXT+d5nyXTVlRm64ZHSgMxMAaPtQJsVYv73m
DWHTzNnLhhINSpgBMLh5a4atM52yxVdkPUTgqIH+LeIPBXn8xaP5JLmfDcmI
tBpc/9DhS3v9iKCX40igAArFu7Gg3swbgQ61SP+U22LvG5nDQZQz3sudtsw3
qKPykFVaYjrRwzvBdSdJ1PwlAsanSwcwS/GKPtmE/ykZ6X5XOx7wvCDL/zGy
B8khkB8hDKC6moCzebmUxCBmTmXD0Wjzon+bf4MOriVE3a0ySGRvpr1mKR2+
9EaVo7pDJLEM487+xg1CAZHRhwshd6II00XEzG/jBQ==
    EXPECTED

    assert_equal expected, [signature].pack("m")
  end

  def test_sign_expired
    signer = Gem::Security::Signer.new PRIVATE_KEY, [EXPIRED_CERT]

    e = assert_raise Gem::Security::Exception do
      signer.sign "hello"
    end

    assert_match "certificate /CN=nobody/DC=example not valid after 1970-01-01 00:00:00 UTC", e.message
  end

  def test_sign_expired_auto_update
    pend if Gem.java_platform?
    FileUtils.mkdir_p File.join(Gem.user_home, ".gem"), :mode => 0700

    private_key_path = File.join(Gem.user_home, ".gem", "gem-private_key.pem")
    Gem::Security.write PRIVATE_KEY, private_key_path

    cert_path = File.join Gem.user_home, ".gem", "gem-public_cert.pem"
    Gem::Security.write EXPIRED_CERT, cert_path

    signer = Gem::Security::Signer.new PRIVATE_KEY, [EXPIRED_CERT]

    signer.sign "hello"

    cert = OpenSSL::X509::Certificate.new File.read cert_path

    refute_equal EXPIRED_CERT.to_pem, cert.to_pem
    assert_in_delta Time.now,         cert.not_before, 10

    expiry = EXPIRED_CERT.not_after.strftime "%Y%m%d%H%M%S"

    expired_path =
      File.join Gem.user_home, ".gem", "gem-public_cert.pem.expired.#{expiry}"

    assert_path_exist expired_path
    assert_equal EXPIRED_CERT.to_pem, File.read(expired_path)
  end

  def test_sign_expired_auto_update_exists
    FileUtils.mkdir_p File.join(Gem.user_home, ".gem"), :mode => 0700

    expiry = EXPIRED_CERT.not_after.strftime "%Y%m%d%H%M%S"
    expired_path =
      File.join Gem.user_home, "gem-public_cert.pem.expired.#{expiry}"

    Gem::Security.write EXPIRED_CERT, expired_path

    private_key_path = File.join(Gem.user_home, "gem-private_key.pem")
    Gem::Security.write PRIVATE_KEY, private_key_path

    cert_path = File.join Gem.user_home, "gem-public_cert.pem"
    Gem::Security.write EXPIRED_CERT, cert_path

    signer = Gem::Security::Signer.new PRIVATE_KEY, [EXPIRED_CERT]

    e = assert_raise Gem::Security::Exception do
      signer.sign "hello"
    end

    assert_match %r{certificate /CN=nobody/DC=example not valid}, e.message
  end

  def test_sign_no_key
    signer = Gem::Security::Signer.new nil, nil

    assert_nil signer.sign "stuff"
  end

  def test_sign_wrong_key
    signer = Gem::Security::Signer.new ALTERNATE_KEY, [PUBLIC_CERT]

    assert_raise Gem::Security::Exception do
      signer.sign "hello"
    end
  end

  def test_sign_no_certs
    signer = Gem::Security::Signer.new ALTERNATE_KEY, []

    assert_raise Gem::Security::Exception do
      signer.sign "hello"
    end
  end
end if Gem::HAVE_OPENSSL
