require 'al-test-utils'

class TestUserPassword < Test::Unit::TestCase
  priority :must

  priority :normal
  def test_valid?
    {
      "{CRYPT}.yNLaKqtwQbnY" => 'wibble', #CRYPT
      "{MD5}DRB9CfW75Ayt495ccenptw==" => 'letmein', #MD5
      "{SMD5}8L2iXJuazftLVHrAf7ptPFQIDaw=" => 'letmein', #SMD5 as generated by slappasswd (4 bytes of salt)
      "{SMD5}kXibTNG+O98gaQtkugYcmSTiE+M2Z5TA" => 'letmein', #SMD5 as generated by Apache Directory Studio (8 bytes of salt)
      "{SMD5}4PkkYH5qI6ydk/9pwvZD3DYwYzVlMzVlLTBkZDEtNGJhMi05NjI5LWRlODgyMDhiMWZmYQ==" => 'letmein', #SMD5 generated with 36 bytes of salt
      "{SHA}t6h1/B6iKLkGEEG3zsS9PFKrPOM=" => 'letmein', #SHA
      "{SSHA}YA87hc9/L/cCGR1HValcJb7a8AYxZXY4" => 'wibble', # SSHA as generated by slappasswd (4 bytes of salt)
      "{SSHA}6J6Ios3l1panY9sm0+g9l3/jFz2kwOPrVA4+OA==" => 'letmein', # SSHA as generated by Apache Directory Studio (8 bytes of salt)
      "{SSHA}f/j1unqoJg1C1zjw8tvxSp4xpow2MGM1ZTM1ZS0wZGQxLTRiYTItOTYyOS1kZTg4MjA4YjFmZmE=" => 'letmein', #SSHA generated with 36 bytes of salt
      "letmein" => 'letmein', #Cleartext password
    }.each do |hash, plain|
      assert_send([ActiveLdap::UserPassword, :valid?,
                   plain, hash])
      assert_not_send([ActiveLdap::UserPassword, :valid?,
                   "not#{plain}", hash])
    end
  end

  sub_test_case("crypt") do
    def test_encrypt
      salt = ".WoUoU9f3IlUx9Hh7D/8y.xA6ziklGib"
      assert_equal("{CRYPT}.W57FZhV52w0s",
                   ActiveLdap::UserPassword.crypt("password", salt))

      password = "PASSWORD"
      hashed_password = ActiveLdap::UserPassword.crypt(password)
      salt = hashed_password.sub(/^\{CRYPT\}/, '')
      assert_equal(hashed_password,
                   ActiveLdap::UserPassword.crypt(password, salt))
    end

    sub_test_case("extract_salt") do
      sub_test_case("base format") do
        def test_less
          message = "salt size must be 2: <a>"
          assert_raise(ArgumentError.new(message)) do
            extract_salt(:crypt, "a")
          end
        end

        def test_exact
          assert_extract_salt(:crypt, "ab", "ab")
        end

        def test_more
          assert_extract_salt(:crypt, "ab", "abc")
        end
      end

      sub_test_case("glibc2 format") do
        sub_test_case("ID") do
          def test_md5
            assert_extract_salt(:crypt, "$1$abcdefgh$", "$1$abcdefgh$")
          end

          def test_blowfish
            assert_extract_salt(:crypt, "$2a$abcdefgh$", "$2a$abcdefgh$")
          end

          def test_sha256
            assert_extract_salt(:crypt, "$5$abcdefgh$", "$5$abcdefgh$")
          end

          def test_sha512
            assert_extract_salt(:crypt, "$6$abcdefgh$", "$6$abcdefgh$")
          end
        end

        sub_test_case("salt") do
          def test_not_teminated
            message = "salt character must be [a-zA-Z0-9./]: <$1>"
            assert_raise(ArgumentError.new(message)) do
              extract_salt(:crypt, "$1$")
            end
          end

          def test_empty
            assert_extract_salt(:crypt, "$1$$", "$1$$")
          end

          def test_lower_case
            assert_extract_salt(:crypt, "$1$abc$", "$1$abc$")
          end

          def test_upper_case
            assert_extract_salt(:crypt, "$1$ABC$", "$1$ABC$")
          end

          def test_digit
            assert_extract_salt(:crypt, "$1$012$", "$1$012$")
          end

          def test_dot
            assert_extract_salt(:crypt, "$1$...$", "$1$...$")
          end

          def test_slash
            assert_extract_salt(:crypt, "$1$///$", "$1$///$")
          end

          def test_mix
            assert_extract_salt(:crypt, "$1$aA0./$", "$1$aA0./$")
          end

          def test_max
            assert_extract_salt(:crypt,
                                "$1$0123456789abcdef$",
                                "$1$0123456789abcdef$")
          end

          def test_over
            message = "salt character must be [a-zA-Z0-9./]: <$1>"
            assert_raise(ArgumentError.new(message)) do
              extract_salt(:crypt, "$1$0123456789abcdefg$")
            end
          end
        end
      end
    end
  end

  def test_md5
    assert_equal("{MD5}X03MO1qnZdYdgyfeuILPmQ==",
                 ActiveLdap::UserPassword.md5("password"))
  end

  def test_smd5
    assert_equal("{SMD5}gjz+SUSfZaux99Xsji/No200cGI=",
                 ActiveLdap::UserPassword.smd5("password", "m4pb"))

    password = "PASSWORD"
    hashed_password = ActiveLdap::UserPassword.smd5(password)
    salt = decode64(hashed_password.sub(/^\{SMD5\}/, ''))[-4, 4]
    assert_equal(hashed_password,
                 ActiveLdap::UserPassword.smd5(password, salt))
  end

  def test_extract_salt_for_smd5
    assert_extract_salt(:smd5, 'this', encode64("1234567890123456this"))
    assert_extract_salt(:smd5, 'this is the salt', encode64("1234567890123456this is the salt"))
    assert_extract_salt(:smd5, nil, encode64("123456789"))
    assert_extract_salt(:smd5, nil, encode64("123456789012345"))
    assert_extract_salt(:smd5, nil, encode64("1234567890123456"))
  end

  def test_sha
    assert_equal("{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
                 ActiveLdap::UserPassword.sha("password"))
  end

  def test_ssha
    assert_equal("{SSHA}ipnlCLA1HaK3mm3hyneJIp+Px2h1RGk3",
                 ActiveLdap::UserPassword.ssha("password", "uDi7"))

    password = "PASSWORD"
    hashed_password = ActiveLdap::UserPassword.ssha(password)
    salt = decode64(hashed_password.sub(/^\{SSHA\}/, ''))[-4, 4]
    assert_equal(hashed_password,
                 ActiveLdap::UserPassword.ssha(password, salt))
  end

  def test_extract_salt_for_ssha
    assert_extract_salt(:ssha, 'this', encode64("12345678901234567890this"))
    assert_extract_salt(:ssha, 'this is the salt', encode64("12345678901234567890this is the salt"))
    assert_extract_salt(:ssha, nil, encode64("12345678901234"))
    assert_extract_salt(:ssha, nil, encode64("1234567890123456789"))
    assert_extract_salt(:ssha, nil, encode64("12345678901234567890"))
  end

  private
  def extract_salt(type, hashed_password)
    ActiveLdap::UserPassword.send("extract_salt_for_#{type}",
                                  hashed_password)
  end
  def assert_extract_salt(type, expected, hashed_password)
    assert_equal(expected, extract_salt(type, hashed_password))
  end

  def encode64(string)
    [string].pack('m').chomp
  end

  def decode64(string)
    string.unpack('m')[0]
  end
end
