# -*- coding: utf-8 -*-

require 'al-test-utils'

class TestLDIF < Test::Unit::TestCase
  include ActiveLdap::GetTextSupport
  include AlTestUtils::Config
  include AlTestUtils::ExampleFile

  priority :must
  def test_integer_attribute_value_to_s
    dn = ActiveLdap::DN.parse("uid=user,ou=People,dc=example,dc=com")
    record = ActiveLdap::Ldif::Record.new(dn, {"uidNumber" => [100]})
    assert_equal(<<-EOL, record.to_s)
dn: uid=user,ou=People,dc=example,dc=com
uidNumber: 100
EOL
  end

  priority :normal
  def test_accept_empty_lines_after_content
    ldif_source = <<-EOL
version: 1
dn: dc=devel,dc=example,dc=com
dc: devel
objectClass: top
objectClass: dcObject
objectClass: organization
o: devel
EOL

    expected_ldif_to_s = <<-EOL
version: 1
dn: dc=devel,dc=example,dc=com
dc: devel
o: devel
objectClass: dcObject
objectClass: organization
objectClass: top
EOL


    assert_ldif_to_s(expected_ldif_to_s, ldif_source)
    assert_ldif_to_s(expected_ldif_to_s, ldif_source + "\n")
    assert_ldif_to_s(expected_ldif_to_s, ldif_source + "\n\n")
    assert_ldif_to_s(expected_ldif_to_s, ldif_source + ("\n" * 10))
  end

  def test_accept_comments_after_content
    ldif_source = <<-EOL
version: 1
dn: dc=devel,dc=example,dc=com
dc: devel
objectClass: top
objectClass: dcObject
objectClass: organization
o: devel

# search result

# numResponse: 1
# numEntries: 1

EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: dc=devel,dc=example,dc=com
dc: devel
o: devel
objectClass: dcObject
objectClass: organization
objectClass: top
EOL
  end

  def test_comments_and_empty_lines_before_content
    ldif_source = <<-EOL
version: 1

#
# LDAPv3
# base <dc=devel,dc=clear-code,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# devel.example.com
dn: dc=devel,dc=example,dc=com
dc: devel
objectClass: top
objectClass: dcObject
objectClass: organization
o: devel
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: dc=devel,dc=example,dc=com
dc: devel
o: devel
objectClass: dcObject
objectClass: organization
objectClass: top
EOL
  end

  def test_unknown_change_type
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: XXX
EOL

    ldif_source_with_error_mark = <<-EOL
changetype: |@|XXX
EOL

    assert_invalid_ldif(["unknown change type: %s", "XXX"],
                        ldif_source, 3, 13, ldif_source_with_error_mark)
  end

  def test_unknown_modify_type
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: modify
XXX: postaladdress
-
EOL

    ldif_source_with_error_mark = <<-EOL
|@|XXX: postaladdress
EOL

    assert_invalid_ldif(["unknown modify type: %s", "XXX"],
                        ldif_source, 4, 1, ldif_source_with_error_mark)
  end

  def test_modify_spec_block_separator_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: modify
add: postaladdress
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
add: postaladdress
|@|
EOL

    assert_invalid_ldif("'-' is missing",
                        ldif_source, 5, 1, ldif_source_with_error_mark)
  end

  def test_modify_spec_first_line_separator_is_missing
    ldif_source = <<-EOL.chomp
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: modify
add: postaladdress
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
add: postaladdress|@|
EOL

    assert_invalid_ldif("separator is missing",
                        ldif_source, 4, 19, ldif_source_with_error_mark)
  end

  def test_modify_target_attribute_name_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: modify
add:
-
EOL

    ldif_source_with_error_mark = <<-EOL
add:|@|
EOL

    assert_invalid_ldif("attribute type is missing",
                        ldif_source, 4, 5, ldif_source_with_error_mark)
  end

  def test_newsuperior_separator_is_missing
    ldif_source = <<-EOL.chomp
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: cn=Paula Jensen
deleteoldrdn: 1
newsuperior: ou=Accounting, dc=airius, dc=com
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
newsuperior: ou=Accounting, dc=airius, dc=com|@|
EOL

    assert_invalid_ldif("separator is missing",
                        ldif_source, 6, 46, ldif_source_with_error_mark)
  end

  def test_newsuperior_value_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: cn=Paula Jensen
deleteoldrdn: 1
newsuperior:
EOL

    ldif_source_with_error_mark = <<-EOL
newsuperior:|@|
EOL

    assert_invalid_ldif("new superior value is missing",
                        ldif_source, 6, 13, ldif_source_with_error_mark)
  end

  def test_deleteoldrdn_separator_is_missing
    ldif_source = <<-EOL.chomp
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: cn=Paula Jensen
deleteoldrdn: 1
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
deleteoldrdn: 1|@|
EOL

    assert_invalid_ldif("separator is missing",
                        ldif_source, 5, 16, ldif_source_with_error_mark)
  end

  def test_invalid_deleteoldrdn_value
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: cn=Paula Jensen
deleteoldrdn: x
EOL

    ldif_source_with_error_mark = <<-EOL
deleteoldrdn: |@|x
EOL

    assert_invalid_ldif("delete old RDN value is missing",
                        ldif_source, 5, 15, ldif_source_with_error_mark)
  end

  def test_deleteoldrdn_value_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: cn=Paula Jensen
deleteoldrdn:
EOL

    ldif_source_with_error_mark = <<-EOL
deleteoldrdn:|@|
EOL

    assert_invalid_ldif("delete old RDN value is missing",
                        ldif_source, 5, 14, ldif_source_with_error_mark)
  end

  def test_deleteoldrdn_mark_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: cn=Paula Jensen
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
newrdn: cn=Paula Jensen
|@|
EOL

    assert_invalid_ldif("'deleteoldrdn:' is missing",
                        ldif_source, 5, 1, ldif_source_with_error_mark)
  end

  def test_newrdn_separator_is_missing
    ldif_source = <<-EOL.chomp
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: cn=Paula Jensen
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
newrdn: cn=Paula Jensen|@|
EOL

    assert_invalid_ldif("separator is missing",
                        ldif_source, 4, 24, ldif_source_with_error_mark)
  end

  def test_newrdn_value_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn:
EOL

    ldif_source_with_error_mark = <<-EOL
newrdn:|@|
EOL

    assert_invalid_ldif("new RDN value is missing",
                        ldif_source, 4, 8, ldif_source_with_error_mark)
  end

  def test_newrdn_mark_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: moddn
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
changetype: moddn
|@|
EOL

    assert_invalid_ldif("'newrdn:' is missing",
                        ldif_source, 4, 1, ldif_source_with_error_mark)
  end

  def test_add_change_type_without_attribute
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: add
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
changetype: add
|@|
EOL

    assert_invalid_ldif("attribute spec is missing",
                        ldif_source, 4, 1, ldif_source_with_error_mark)
  end

  def test_change_type_with_an_extra_space
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: delete 
EOL

    ldif_source_with_error_mark = <<-EOL
changetype: delete|@| 
EOL

    assert_invalid_ldif("separator is missing",
                        ldif_source, 3, 19, ldif_source_with_error_mark)
  end

  def test_change_type_separator_is_missing
    ldif_source = <<-EOL.chomp
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: delete
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
changetype: delete|@|
EOL

    assert_invalid_ldif("separator is missing",
                        ldif_source, 3, 19, ldif_source_with_error_mark)
  end

  def test_change_type_value_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype:
EOL

    ldif_source_with_error_mark = <<-EOL
changetype:|@|
EOL

    assert_invalid_ldif("change type value is missing",
                        ldif_source, 3, 12, ldif_source_with_error_mark)
  end

  def test_control_separator_is_missing
    ldif_source = <<-EOL.chomp
version: 1
dn: ou=Product Development, dc=airius, dc=com
control: 1.2.840.113556.1.4.805 true
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
control: 1.2.840.113556.1.4.805 true|@|
EOL

    assert_invalid_ldif("separator is missing",
                        ldif_source, 3, 37, ldif_source_with_error_mark)
  end

  def test_criticality_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
control: 1.2.840.113556.1.4.805 
EOL

    ldif_source_with_error_mark = <<-EOL
control: 1.2.840.113556.1.4.805 |@|
EOL

    assert_invalid_ldif("criticality is missing",
                        ldif_source, 3, 33, ldif_source_with_error_mark)
  end

  def test_control_type_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
control:
EOL

    ldif_source_with_error_mark = <<-EOL
control:|@|
EOL

    assert_invalid_ldif("control type is missing",
                        ldif_source, 3, 9, ldif_source_with_error_mark)
  end

  def test_change_type_is_missing
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
control: 1.2.840.113556.1.4.805 true
EOL

    ldif_source_with_error_mark = <<-EOL.chomp
control: 1.2.840.113556.1.4.805 true
|@|
EOL

    assert_invalid_ldif("change type is missing",
                        ldif_source, 4, 1, ldif_source_with_error_mark)
  end

  def test_invalid_dn
    ldif_source = <<-EOL
version: 1
dn: ou=o=Airius
EOL

    ldif_source_with_error_mark = <<-EOL
dn: ou=o=Airius|@|
EOL

    assert_invalid_ldif(["DN is invalid: %s: %s",
                         "ou=o=Airius", "attribute type is missing"],
                        ldif_source, 2, 16, ldif_source_with_error_mark)
  end

  def test_invalid_dn_value
    ldif_source = <<-EOL
version: 1
# dn:: ou=<JapaneseOU>,o=Airius
dn: ou=営業部,o=Airius
EOL

    ldif_source_with_error_mark = <<-EOL
dn: ou=|@|営業部,o=Airius
EOL

    assert_invalid_ldif(["DN has an invalid character: %s", "営"],
                        ldif_source, 3, 8, ldif_source_with_error_mark)
  end

  def test_multi_records_without_separator
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development, dc=airius, dc=com
changetype: delete
dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com
seealso:
description::
EOL

    ldif_source_with_error_mark = <<-EOL
|@|dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com
EOL

    assert_invalid_ldif("separator is missing", ldif_source,
                        4, 1, ldif_source_with_error_mark)
  end

  def test_to_s_with_blank_value
    ldif_source = <<-EOL
version: 1
dn: ou=Product Development,dc=airius,dc=com
seealso:
description::
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: ou=Product Development,dc=airius,dc=com
description:
seealso:
EOL
  end

  def test_to_s_with_last_space
    ldif_source = <<-EOL
version: 1
# 'ou=Product Development,dc=airius,dc=com '
dn:: b3U9UHJvZHVjdCBEZXZlbG9wbWVudCwgZGM9YWlyaXVzLCBkYz1jb20g
# 'ou=Product Development,dc=airius,dc=com '
description:: b3U9UHJvZHVjdCBEZXZlbG9wbWVudCwgZGM9YWlyaXVzLCBkYz1jb20g
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: ou=Product Development,dc=airius,dc="com "
description:: b3U9UHJvZHVjdCBEZXZlbG9wbWVudCwgZGM9YWlyaXVzLCBkYz1jb20g
EOL
  end

  def test_change_record_with_control
    ldif_source = <<-EOL
version: 1
# Delete an entry. The operation will attach the LDAPv3
# Tree Delete Control defined in [9]. The criticality
# field is "true" and the controlValue field is
# absent, as required by [9].
dn: ou=Product Development, dc=airius, dc=com
control: 1.2.840.113556.1.4.805 true
changetype: delete
EOL

    change_attributes = {
      "dn" => "ou=Product Development,dc=airius,dc=com",
    }

    ldif = assert_ldif(1, [change_attributes], ldif_source)
    record = ldif.records[0]
    assert_equal("delete", record.change_type)
    assert_true(record.delete?)
    assert_equal([{
                    :type => "1.2.840.113556.1.4.805",
                    :criticality => true,
                    :value => nil
                  }],
                 record.controls.collect {|control| control.to_hash})

    control = record.controls[0]
    assert_equal("1.2.840.113556.1.4.805", control.type)
    assert_true(control.criticality?)
    assert_nil(control.value)
  end

  def test_change_record_with_control_to_s
    ldif_source = <<-EOL
version: 1
# Delete an entry. The operation will attach the LDAPv3
# Tree Delete Control defined in [9]. The criticality
# field is "true" and the controlValue field is
# absent, as required by [9].
dn: ou=Product Development, dc=airius, dc=com
control: 1.2.840.113556.1.4.805 true
changetype: delete
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: ou=Product Development,dc=airius,dc=com
control: 1.2.840.113556.1.4.805 true
changetype: delete
EOL
  end

  def test_multi_change_type_records
    ldif_source = <<-EOL
version: 1
# Add a new entry
dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com
changetype: add
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Fiona Jensen
sn: Jensen
uid: fiona
telephonenumber: +1 408 555 1212

# Delete an existing entry
dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com
changetype: delete

# Modify an entry's relative distinguished name
dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com
changetype: modrdn
newrdn: cn=Paula Jensen
deleteoldrdn: 1

# Rename an entry and move all of its children to a new location in
# the directory tree (only implemented by LDAPv3 servers).
dn: ou=PD Accountants, ou=Product Development, dc=airius, dc=com
changetype: modrdn
newrdn: ou=Product Development Accountants
deleteoldrdn: 0
newsuperior: ou=Accounting, dc=airius, dc=com

# Modify an entry: add an additional value to the postaladdress
# attribute, completely delete the description attribute, replace
# the telephonenumber attribute with two values, and delete a specific
# value from the facsimiletelephonenumber attribute
dn: cn=Paula Jensen, ou=Product Development, dc=airius, dc=com
changetype: modify
add: postaladdress
postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086
-
delete: description
-
replace: telephonenumber
telephonenumber: +1 408 555 1234
telephonenumber: +1 408 555 5678
-
delete: facsimiletelephonenumber
facsimiletelephonenumber: +1 408 555 9876
-

# Modify an entry: replace the postaladdress attribute with an empty
# set of values (which will cause the attribute to be removed), and
# delete the entire description attribute. Note that the first will
# always succeed, while the second will only succeed if at least
# one value for the description attribute is present.
dn: cn=Ingrid Jensen, ou=Product Support, dc=airius, dc=com
changetype: modify
replace: postaladdress
-
delete: description
-
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Fiona Jensen,ou=Marketing,dc=airius,dc=com
changetype: add
cn: Fiona Jensen
objectclass: organizationalPerson
objectclass: person
objectclass: top
sn: Jensen
telephonenumber: +1 408 555 1212
uid: fiona

dn: cn=Robert Jensen,ou=Marketing,dc=airius,dc=com
changetype: delete

dn: cn=Paul Jensen,ou=Product Development,dc=airius,dc=com
changetype: modrdn
newrdn: cn=Paula Jensen
deleteoldrdn: 1

dn: ou=PD Accountants,ou=Product Development,dc=airius,dc=com
changetype: modrdn
newrdn: ou=Product Development Accountants
deleteoldrdn: 0
newsuperior: ou=Accounting,dc=airius,dc=com

dn: cn=Paula Jensen,ou=Product Development,dc=airius,dc=com
changetype: modify
add: postaladdress
postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086
-
delete: description
-
replace: telephonenumber
telephonenumber: +1 408 555 1234
telephonenumber: +1 408 555 5678
-
delete: facsimiletelephonenumber
facsimiletelephonenumber: +1 408 555 9876
-

dn: cn=Ingrid Jensen,ou=Product Support,dc=airius,dc=com
changetype: modify
replace: postaladdress
-
delete: description
-
EOL

    change_attributes_add = {
      "dn" => "cn=Fiona Jensen,ou=Marketing,dc=airius,dc=com",
      "objectclass" => ["top", "person", "organizationalPerson"],
      "cn" => ["Fiona Jensen"],
      "sn" => ["Jensen"],
      "uid" => ["fiona"],
      "telephonenumber" => ["+1 408 555 1212"],
    }

    change_attributes_delete = {
      "dn" => "cn=Robert Jensen,ou=Marketing,dc=airius,dc=com",
    }

    change_attributes_modrdn = {
      "dn" => "cn=Paul Jensen,ou=Product Development,dc=airius,dc=com",
    }

    change_attributes_modrdn_with_new_superior = {
      "dn" => "ou=PD Accountants,ou=Product Development,dc=airius,dc=com",
    }

    change_attributes_modify = {
      "dn" => "cn=Paula Jensen,ou=Product Development,dc=airius,dc=com",
    }

    change_attributes_modify_with_empty_replace = {
      "dn" => "cn=Ingrid Jensen,ou=Product Support,dc=airius,dc=com",
    }

    ldif = assert_ldif(1,
                       [change_attributes_add,
                        change_attributes_delete,
                        change_attributes_modrdn,
                        change_attributes_modrdn_with_new_superior,
                        change_attributes_modify,
                        change_attributes_modify_with_empty_replace],
                       ldif_source)
    record = ldif.records[0]
    assert_equal("add", record.change_type)
    assert_true(record.add?)

    record = ldif.records[1]
    assert_equal("delete", record.change_type)
    assert_true(record.delete?)

    record = ldif.records[2]
    assert_equal("modrdn", record.change_type)
    assert_true(record.modify_rdn?)
    assert_equal("cn=Paula Jensen", record.new_rdn)
    assert_true(record.delete_old_rdn?)
    assert_nil(record.new_superior)

    record = ldif.records[3]
    assert_equal("modrdn", record.change_type)
    assert_true(record.modify_rdn?)
    assert_equal("ou=Product Development Accountants", record.new_rdn)
    assert_false(record.delete_old_rdn?)
    assert_equal("ou=Accounting,dc=airius,dc=com", record.new_superior)

    record = ldif.records[4]
    assert_equal("modify", record.change_type)
    assert_true(record.modify?)
    operations = [
                  ["add", "postaladdress",
                   {"postaladdress" =>
                     ["123 Anystreet $ Sunnyvale, CA $ 94086"]}],
                  ["delete", "description", {}],
                  ["replace", "telephonenumber",
                   {"telephonenumber" => [
                                          "+1 408 555 1234",
                                          "+1 408 555 5678",
                                         ]}],
                  ["delete", "facsimiletelephonenumber",
                   {"facsimiletelephonenumber" => ["+1 408 555 9876"]}],
                 ]
    i = -1
    actual = record.operations.collect do |operation|
      i += 1
      type = operations[i][0]
      [operation.send("#{type}?"),
       [operation.type, operation.attribute, operation.attributes]]
    end
    assert_equal(operations.collect {|operation| [true, operation]},
                 actual)

    record = ldif.records[5]
    assert_equal("modify", record.change_type)
    assert_true(record.modify?)
    operations = [
                  ["replace", "postaladdress", {}],
                  ["delete", "description", {}],
                 ]
    i = -1
    actual = record.operations.collect do |operation|
      i += 1
      type = operations[i][0]
      [operation.send("#{type}?"),
       [operation.type, operation.attribute, operation.attributes]]
    end
    assert_equal(operations.collect {|operation| [true, operation]},
                 actual)
  end

  def test_modify_record
    ldif_source = <<-EOL
version: 1
# Modify an entry: add an additional value to the postaladdress
# attribute, completely delete the description attribute, replace
# the telephonenumber attribute with two values, and delete a specific
# value from the facsimiletelephonenumber attribute
dn: cn=Paula Jensen, ou=Product Development, dc=airius, dc=com
changetype: modify
add: postaladdress
postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086
-
delete: description
-
replace: telephonenumber
telephonenumber: +1 408 555 1234
telephonenumber: +1 408 555 5678
-
delete: facsimiletelephonenumber
facsimiletelephonenumber: +1 408 555 9876
-
EOL

    change_attributes = {
      "dn" => "cn=Paula Jensen,ou=Product Development,dc=airius,dc=com",
    }

    ldif = assert_ldif(1, [change_attributes], ldif_source)
    record = ldif.records[0]
    assert_equal("modify", record.change_type)
    assert_true(record.modify?)

    operations = [
                  ["add", "postaladdress",
                   {"postaladdress" =>
                     ["123 Anystreet $ Sunnyvale, CA $ 94086"]}],
                  ["delete", "description", {}],
                  ["replace", "telephonenumber",
                   {"telephonenumber" => [
                                          "+1 408 555 1234",
                                          "+1 408 555 5678",
                                         ]}],
                  ["delete", "facsimiletelephonenumber",
                   {"facsimiletelephonenumber" => ["+1 408 555 9876"]}],
                 ]
    i = -1
    actual = record.operations.collect do |operation|
      i += 1
      type = operations[i][0]
      [operation.send("#{type}?"),
       [operation.type, operation.attribute, operation.attributes]]
    end
    assert_equal(operations.collect {|operation| [true, operation]},
                 actual)
  end

  def test_modify_record_to_s
    ldif_source = <<-EOL
version: 1
# Modify an entry: add an additional value to the postaladdress
# attribute, completely delete the description attribute, replace
# the telephonenumber attribute with two values, and delete a specific
# value from the facsimiletelephonenumber attribute
dn: cn=Paula Jensen, ou=Product Development, dc=airius, dc=com
changetype: modify
add: postaladdress
postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086
-
delete: description
-
replace: telephonenumber
telephonenumber: +1 408 555 1234
telephonenumber: +1 408 555 5678
-
delete: facsimiletelephonenumber
facsimiletelephonenumber: +1 408 555 9876
-
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Paula Jensen,ou=Product Development,dc=airius,dc=com
changetype: modify
add: postaladdress
postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086
-
delete: description
-
replace: telephonenumber
telephonenumber: +1 408 555 1234
telephonenumber: +1 408 555 5678
-
delete: facsimiletelephonenumber
facsimiletelephonenumber: +1 408 555 9876
-
EOL
  end

  def test_modrdn_record_with_newsuperior
    ldif_source = <<-EOL
version: 1
# Rename an entry and move all of its children to a new location in
# the directory tree (only implemented by LDAPv3 servers).
dn: ou=PD Accountants, ou=Product Development, dc=airius, dc=com
changetype: modrdn
newrdn: ou=Product Development Accountants
deleteoldrdn: 0
newsuperior: ou=Accounting, dc=airius, dc=com
EOL

    change_attributes = {
      "dn" => "ou=PD Accountants,ou=Product Development,dc=airius,dc=com",
    }

    ldif = assert_ldif(1, [change_attributes], ldif_source)
    record = ldif.records[0]
    assert_equal("modrdn", record.change_type)
    assert_true(record.modify_rdn?)
    assert_equal("ou=Product Development Accountants", record.new_rdn)
    assert_false(record.delete_old_rdn?)
    assert_equal("ou=Accounting,dc=airius,dc=com", record.new_superior)
  end

  def test_modrdn_record_with_newsuperior_to_s
    ldif_source = <<-EOL
version: 1
# Rename an entry and move all of its children to a new location in
# the directory tree (only implemented by LDAPv3 servers).
dn: ou=PD Accountants, ou=Product Development, dc=airius, dc=com
changetype: modrdn
newrdn: ou=Product Development Accountants
deleteoldrdn: 0
newsuperior: ou=Accounting, dc=airius, dc=com
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: ou=PD Accountants,ou=Product Development,dc=airius,dc=com
changetype: modrdn
newrdn: ou=Product Development Accountants
deleteoldrdn: 0
newsuperior: ou=Accounting,dc=airius,dc=com
EOL
  end

  def test_modrdn_record
    ldif_source = <<-EOL
version: 1
# Modify an entry's relative distinguished name
dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com
changetype: modrdn
newrdn: cn=Paula Jensen
deleteoldrdn: 1
EOL

    change_attributes = {
      "dn" => "cn=Paul Jensen,ou=Product Development,dc=airius,dc=com",
    }

    ldif = assert_ldif(1, [change_attributes], ldif_source)
    record = ldif.records[0]
    assert_equal("modrdn", record.change_type)
    assert_true(record.modify_rdn?)
    assert_equal("cn=Paula Jensen", record.new_rdn)
    assert_true(record.delete_old_rdn?)
    assert_nil(record.new_superior)
  end

  def test_modrdn_record_to_s
    ldif_source = <<-EOL
version: 1
# Modify an entry's relative distinguished name
dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com
changetype: modrdn
newrdn: cn=Paula Jensen
deleteoldrdn: 1
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Paul Jensen,ou=Product Development,dc=airius,dc=com
changetype: modrdn
newrdn: cn=Paula Jensen
deleteoldrdn: 1
EOL
  end

  def test_moddn_record_with_newsuperior
    ldif_source = <<-EOL
version: 1
# Rename an entry and move all of its children to a new location in
# the directory tree (only implemented by LDAPv3 servers).
dn: ou=PD Accountants, ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: ou=Product Development Accountants
deleteoldrdn: 0
newsuperior: ou=Accounting, dc=airius, dc=com
EOL

    change_attributes = {
      "dn" => "ou=PD Accountants,ou=Product Development,dc=airius,dc=com",
    }

    ldif = assert_ldif(1, [change_attributes], ldif_source)
    record = ldif.records[0]
    assert_equal("moddn", record.change_type)
    assert_true(record.modify_dn?)
    assert_equal("ou=Product Development Accountants", record.new_rdn)
    assert_false(record.delete_old_rdn?)
    assert_equal("ou=Accounting,dc=airius,dc=com", record.new_superior)
  end

  def test_moddn_record_with_newsuperior_to_s
    ldif_source = <<-EOL
version: 1
# Rename an entry and move all of its children to a new location in
# the directory tree (only implemented by LDAPv3 servers).
dn: ou=PD Accountants, ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: ou=Product Development Accountants
deleteoldrdn: 0
newsuperior: ou=Accounting, dc=airius, dc=com
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: ou=PD Accountants,ou=Product Development,dc=airius,dc=com
changetype: moddn
newrdn: ou=Product Development Accountants
deleteoldrdn: 0
newsuperior: ou=Accounting,dc=airius,dc=com
EOL
  end

  def test_moddn_record
    ldif_source = <<-EOL
version: 1
# Modify an entry's relative distinguished name
dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: cn=Paula Jensen
deleteoldrdn: 1
EOL

    change_attributes = {
      "dn" => "cn=Paul Jensen,ou=Product Development,dc=airius,dc=com",
    }

    ldif = assert_ldif(1, [change_attributes], ldif_source)
    record = ldif.records[0]
    assert_equal("moddn", record.change_type)
    assert_true(record.modify_dn?)
    assert_equal("cn=Paula Jensen", record.new_rdn)
    assert_true(record.delete_old_rdn?)
    assert_nil(record.new_superior)
  end

  def test_moddn_record_to_s
    ldif_source = <<-EOL
version: 1
# Modify an entry's relative distinguished name
dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com
changetype: moddn
newrdn: cn=Paula Jensen
deleteoldrdn: 1
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Paul Jensen,ou=Product Development,dc=airius,dc=com
changetype: moddn
newrdn: cn=Paula Jensen
deleteoldrdn: 1
EOL
  end

  def test_delete_record
    ldif_source = <<-EOL
version: 1
# Delete an existing entry
dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com
changetype: delete
EOL

    change_attributes = {
      "dn" => "cn=Robert Jensen,ou=Marketing,dc=airius,dc=com",
    }

    ldif = assert_ldif(1, [change_attributes], ldif_source)
    record = ldif.records[0]
    assert_equal("delete", record.change_type)
    assert_true(record.delete?)
  end

  def test_delete_record_to_s
    ldif_source = <<-EOL
version: 1
# Delete an existing entry
dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com
changetype: delete
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Robert Jensen,ou=Marketing,dc=airius,dc=com
changetype: delete
EOL
  end

  def test_add_record
    ldif_source = <<-EOL
version: 1
# Add a new entry
dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com
changetype: add
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Fiona Jensen
sn: Jensen
uid: fiona
telephonenumber: +1 408 555 1212
EOL

    change_attributes = {
      "dn" => "cn=Fiona Jensen,ou=Marketing,dc=airius,dc=com",
      "objectclass" => ["top", "person", "organizationalPerson"],
      "cn" => ["Fiona Jensen"],
      "sn" => ["Jensen"],
      "uid" => ["fiona"],
      "telephonenumber" => ["+1 408 555 1212"],
    }

    ldif = assert_ldif(1, [change_attributes], ldif_source)
    record = ldif.records[0]
    assert_equal("add", record.change_type)
    assert_true(record.add?)
  end

  def test_add_record_to_s
    ldif_source = <<-EOL
version: 1
# Add a new entry
dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com
changetype: add
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Fiona Jensen
sn: Jensen
uid: fiona
telephonenumber: +1 408 555 1212
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Fiona Jensen,ou=Marketing,dc=airius,dc=com
changetype: add
cn: Fiona Jensen
objectclass: organizationalPerson
objectclass: person
objectclass: top
sn: Jensen
telephonenumber: +1 408 555 1212
uid: fiona
EOL
  end

  def test_records_with_external_file_reference
    ldif_source = <<-EOL
version: 1
dn: cn=Horatio Jensen, ou=Product Testing, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Horatio Jensen
cn: Horatio N Jensen
sn: Jensen
uid: hjensen
telephonenumber: +1 408 555 1212
jpegphoto:< file://#{jpeg_photo_path}
EOL

    record = {
      "dn" => "cn=Horatio Jensen,ou=Product Testing,dc=airius,dc=com",
      "objectclass" => ["top", "person", "organizationalPerson"],
      "cn" => ["Horatio Jensen", "Horatio N Jensen"],
      "sn" => ["Jensen"],
      "uid" => ["hjensen"],
      "telephonenumber" => ["+1 408 555 1212"],
      "jpegphoto" => [jpeg_photo],
    }

    assert_ldif(1, [record], ldif_source)
  end

  def test_records_with_external_file_reference_to_s
    ldif_source = <<-EOL
version: 1
dn: cn=Horatio Jensen, ou=Product Testing, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Horatio Jensen
cn: Horatio N Jensen
sn: Jensen
uid: hjensen
telephonenumber: +1 408 555 1212
jpegphoto:< file://#{jpeg_photo_path}
EOL
    set_encoding(ldif_source, "utf-8")

    jpeg_photo_attribute = "jpegphoto:: "
    value = [jpeg_photo].pack("m").gsub(/\n/, '')
    first_line_value_size = 75 - jpeg_photo_attribute.size
    jpeg_photo_attribute << value[0, first_line_value_size] + "\n"
    value = value[first_line_value_size..-1]
    value.scan(/.{1,74}/).each do |line|
      jpeg_photo_attribute << " #{line}\n"
    end
    jpeg_photo_attribute = jpeg_photo_attribute.chomp

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Horatio Jensen,ou=Product Testing,dc=airius,dc=com
cn: Horatio Jensen
cn: Horatio N Jensen
#{jpeg_photo_attribute}
objectclass: organizationalPerson
objectclass: person
objectclass: top
sn: Jensen
telephonenumber: +1 408 555 1212
uid: hjensen
EOL
  end

  def test_record_with_external_file_reference_is_invalid
    ldif_source = <<-EOL
version: 1
dn: cn=Horatio Jensen, ou=Product Testing, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Horatio Jensen
sn: Jensen
uid: hjensen
jpegphoto:< INVALID_URI
EOL

    ldif_source_with_error_mark = <<-EOL
jpegphoto:< |@|INVALID_URI
EOL

    assert_invalid_ldif("URI is missing",
                        ldif_source, 9, 13, ldif_source_with_error_mark)
  end

  def test_records_with_option_attributes
    ldif_source = <<-EOL
version: 1
dn:: b3U95Za25qWt6YOoLG89QWlyaXVz
# dn:: ou=<JapaneseOU>,o=Airius
objectclass: top
objectclass: organizationalUnit
ou:: 5Za25qWt6YOo
# ou:: <JapaneseOU>
ou;lang-ja:: 5Za25qWt6YOo
# ou;lang-ja:: <JapaneseOU>
ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2
# ou;lang-ja:: <JapaneseOU_in_phonetic_representation>
ou;lang-en: Sales
description: Japanese office

dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz
# dn:: uid=<uid>,ou=<JapaneseOU>,o=Airius
userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM=
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
uid: rogasawara
mail: rogasawara@airius.co.jp
givenname;lang-ja:: 44Ot44OJ44OL44O8
# givenname;lang-ja:: <JapaneseGivenname>
sn;lang-ja:: 5bCP56yg5Y6f
# sn;lang-ja:: <JapaneseSn>
cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
# cn;lang-ja:: <JapaneseCn>
title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw==
# title;lang-ja:: <JapaneseTitle>
preferredlanguage: ja
givenname:: 44Ot44OJ44OL44O8
# givenname:: <JapaneseGivenname>
sn:: 5bCP56yg5Y6f
# sn:: <JapaneseSn>
cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
# cn:: <JapaneseCn>
title:: 5Za25qWt6YOoIOmDqOmVtw==
# title:: <JapaneseTitle>
givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8
# givenname;lang-ja;phonetic::
  <JapaneseGivenname_in_phonetic_representation_kana>
sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ
# sn;lang-ja;phonetic:: <JapaneseSn_in_phonetic_representation_kana>
cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA==
# cn;lang-ja;phonetic:: <JapaneseCn_in_phonetic_representation_kana>
title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg==
# title;lang-ja;phonetic::
# <JapaneseTitle_in_phonetic_representation_kana>
givenname;lang-en: Rodney
sn;lang-en: Ogasawara
cn;lang-en: Rodney Ogasawara
title;lang-en: Sales, Director
EOL
    set_encoding(ldif_source, "utf-8")

    record1 = {
      "dn" => "ou=営業部,o=Airius",
      "objectclass" => ["top", "organizationalUnit"],
      "ou" => [
               "営業部",
               {"lang-ja" =>
                 [
                  "営業部",
                  {"phonetic" => ["えいぎょうぶ"]},
                 ],
               },
               {"lang-en" => ["Sales"]},
              ],
      "description" => ["Japanese office"],
    }

    record2 = {
      "dn" => "uid=rogasawara,ou=営業部,o=Airius",
      "userpassword" => ["{SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM="],
      "objectclass" => ["top", "person",
                        "organizationalPerson", "inetOrgPerson"],
      "uid" => ["rogasawara"],
      "mail" => ["rogasawara@airius.co.jp"],
      "givenname" => [
                      {"lang-ja" =>
                        [
                         "ロドニー",
                         {"phonetic" => ["ろどにー"]},
                        ]
                      },
                      "ロドニー",
                      {"lang-en" => ["Rodney"]},
                     ],
      "sn" => [
               {"lang-ja" =>
                 [
                  "小笠原",
                  {"phonetic" => ["おがさわら"]},
                 ]
               },
               "小笠原",
               {"lang-en" => ["Ogasawara"]},
              ],
      "cn" => [
               {"lang-ja" =>
                 [
                  "小笠原 ロドニー",
                  {"phonetic" => ["おがさわら ろどにー"]},
                 ],
               },
               "小笠原 ロドニー",
               {"lang-en" => ["Rodney Ogasawara"]},
              ],
      "title" => [
                  {"lang-ja" =>
                    [
                     "営業部 部長",
                     {"phonetic" => ["えいぎょうぶ ぶちょう"]}
                    ]
                  },
                  "営業部 部長",
                  {"lang-en" => ["Sales, Director"]},
                 ],
      "preferredlanguage" => ["ja"],
    }

    assert_ldif(1, [record1, record2], ldif_source)
  end

  def test_records_with_option_attributes_to_s
    ldif_source = <<-EOL
version: 1
dn:: b3U95Za25qWt6YOoLG89QWlyaXVz
# dn:: ou=<JapaneseOU>,o=Airius
objectclass: top
objectclass: organizationalUnit
ou:: 5Za25qWt6YOo
# ou:: <JapaneseOU>
ou;lang-ja:: 5Za25qWt6YOo
# ou;lang-ja:: <JapaneseOU>
ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2
# ou;lang-ja:: <JapaneseOU_in_phonetic_representation>
ou;lang-en: Sales
description: Japanese office

dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz
# dn:: uid=<uid>,ou=<JapaneseOU>,o=Airius
userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM=
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
uid: rogasawara
mail: rogasawara@airius.co.jp
givenname;lang-ja:: 44Ot44OJ44OL44O8
# givenname;lang-ja:: <JapaneseGivenname>
sn;lang-ja:: 5bCP56yg5Y6f
# sn;lang-ja:: <JapaneseSn>
cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
# cn;lang-ja:: <JapaneseCn>
title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw==
# title;lang-ja:: <JapaneseTitle>
preferredlanguage: ja
givenname:: 44Ot44OJ44OL44O8
# givenname:: <JapaneseGivenname>
sn:: 5bCP56yg5Y6f
# sn:: <JapaneseSn>
cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
# cn:: <JapaneseCn>
title:: 5Za25qWt6YOoIOmDqOmVtw==
# title:: <JapaneseTitle>
givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8
# givenname;lang-ja;phonetic::
  <JapaneseGivenname_in_phonetic_representation_kana>
sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ
# sn;lang-ja;phonetic:: <JapaneseSn_in_phonetic_representation_kana>
cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA==
# cn;lang-ja;phonetic:: <JapaneseCn_in_phonetic_representation_kana>
title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg==
# title;lang-ja;phonetic::
# <JapaneseTitle_in_phonetic_representation_kana>
givenname;lang-en: Rodney
sn;lang-en: Ogasawara
cn;lang-en: Rodney Ogasawara
title;lang-en: Sales, Director
EOL
    set_encoding(ldif_source, "utf-8")

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn:: b3U95Za25qWt6YOoLG89QWlyaXVz
description: Japanese office
objectclass: organizationalUnit
objectclass: top
ou:: 5Za25qWt6YOo
ou;lang-en: Sales
ou;lang-ja:: 5Za25qWt6YOo
ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2

dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz
cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
cn;lang-en: Rodney Ogasawara
cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA==
givenname:: 44Ot44OJ44OL44O8
givenname;lang-en: Rodney
givenname;lang-ja:: 44Ot44OJ44OL44O8
givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8
mail: rogasawara@airius.co.jp
objectclass: inetOrgPerson
objectclass: organizationalPerson
objectclass: person
objectclass: top
preferredlanguage: ja
sn:: 5bCP56yg5Y6f
sn;lang-en: Ogasawara
sn;lang-ja:: 5bCP56yg5Y6f
sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ
title:: 5Za25qWt6YOoIOmDqOmVtw==
title;lang-en: Sales, Director
title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw==
title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg==
uid: rogasawara
userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM=
EOL
  end

  def test_an_record_with_base64_encoded_value
    ldif_source = <<-EOL
version: 1
dn: cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Gern Jensen
cn: Gern O Jensen
sn: Jensen
uid: gernj
telephonenumber: +1 408 555 1212
description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl
 IGlzIGJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG
 VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg
 b3V0IG1vcmUu
EOL

    record = {
      "dn" => "cn=Gern Jensen,ou=Product Testing,dc=airius,dc=com",
      "objectclass" => ["top", "person", "organizationalPerson"],
      "cn" => ["Gern Jensen", "Gern O Jensen"],
      "sn" => ["Jensen"],
      "uid" => ["gernj"],
      "telephonenumber" => ["+1 408 555 1212"],
      "description" => ["What a careful reader you are!  " +
                        "This value is base-64-encoded because it has a " +
                        "control character in it (a CR).\r  By the way, " +
                        "you should really get out more."],
    }
    assert_ldif(1, [record], ldif_source)
  end

  def test_an_record_with_base64_encoded_value_to_s
    ldif_source = <<-EOL
version: 1
dn: cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Gern Jensen
cn: Gern O Jensen
sn: Jensen
uid: gernj
telephonenumber: +1 408 555 1212
description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl
 IGlzIGJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG
 VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg
 b3V0IG1vcmUu
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Gern Jensen,ou=Product Testing,dc=airius,dc=com
cn: Gern Jensen
cn: Gern O Jensen
description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVlIGlzI
 GJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdGVyIGluIGl
 0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQgb3V0IG1vcmUu
objectclass: organizationalPerson
objectclass: person
objectclass: top
sn: Jensen
telephonenumber: +1 408 555 1212
uid: gernj
EOL
  end

  def test_an_record_with_folded_attribute_value
    ldif_source = <<-EOL
version: 1
dn:cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
objectclass:top
objectclass:person
objectclass:organizationalPerson
cn:Barbara Jensen
cn:Barbara J Jensen
cn:Babs Jensen
sn:Jensen
uid:bjensen
telephonenumber:+1 408 555 1212
description:Babs is a big sailing fan, and travels extensively in sea
 rch of perfect sailing conditions.
title:Product Manager, Rod and Reel Division
EOL

    record = {
      "dn" => "cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com",
      "objectclass" => ["top", "person", "organizationalPerson"],
      "cn" => ["Barbara Jensen", "Barbara J Jensen", "Babs Jensen"],
      "sn" => ["Jensen"],
      "uid" => ["bjensen"],
      "telephonenumber" => ["+1 408 555 1212"],
      "description" => ["Babs is a big sailing fan, and travels extensively " +
                        "in search of perfect sailing conditions."],
      "title" => ["Product Manager, Rod and Reel Division"],
    }
    assert_ldif(1, [record], ldif_source)
  end

  def test_an_record_with_folded_attribute_value_to_s
    ldif_source = <<-EOL
version: 1
dn:cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
objectclass:top
objectclass:person
objectclass:organizationalPerson
cn:Barbara Jensen
cn:Barbara J Jensen
cn:Babs Jensen
sn:Jensen
uid:bjensen
telephonenumber:+1 408 555 1212
description:Babs is a big sailing fan, and travels extensively in sea
 rch of perfect sailing conditions.
title:Product Manager, Rod and Reel Division
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com
cn: Babs Jensen
cn: Barbara J Jensen
cn: Barbara Jensen
description: Babs is a big sailing fan, and travels extensively in search o
 f perfect sailing conditions.
objectclass: organizationalPerson
objectclass: person
objectclass: top
sn: Jensen
telephonenumber: +1 408 555 1212
title: Product Manager, Rod and Reel Division
uid: bjensen
EOL
  end

  def test_records
    ldif_source = <<-EOL
version: 1
dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Barbara Jensen
cn: Barbara J Jensen
cn: Babs Jensen
sn: Jensen
uid: bjensen
telephonenumber: +1 408 555 1212
description: A big sailing fan.

dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Bjorn Jensen
sn: Jensen
telephonenumber: +1 408 555 1212
EOL

    record1 = {
      "dn" => "cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com",
      "objectclass" => ["top", "person", "organizationalPerson"],
      "cn" => ["Barbara Jensen", "Barbara J Jensen", "Babs Jensen"],
      "sn" => ["Jensen"],
      "uid" => ["bjensen"],
      "telephonenumber" => ["+1 408 555 1212"],
      "description" => ["A big sailing fan."],
    }
    record2 = {
      "dn" => "cn=Bjorn Jensen,ou=Accounting,dc=airius,dc=com",
      "objectclass" => ["top", "person", "organizationalPerson"],
      "cn" => ["Bjorn Jensen"],
      "sn" => ["Jensen"],
      "telephonenumber" => ["+1 408 555 1212"],
    }
    assert_ldif(1, [record1, record2], ldif_source)
  end

  def test_records_to_s
    ldif_source = <<-EOL
version: 1
dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Barbara Jensen
cn: Barbara J Jensen
cn: Babs Jensen
sn: Jensen
uid: bjensen
telephonenumber: +1 408 555 1212
description: A big sailing fan.

dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Bjorn Jensen
sn: Jensen
telephonenumber: +1 408 555 1212
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com
cn: Babs Jensen
cn: Barbara J Jensen
cn: Barbara Jensen
description: A big sailing fan.
objectclass: organizationalPerson
objectclass: person
objectclass: top
sn: Jensen
telephonenumber: +1 408 555 1212
uid: bjensen

dn: cn=Bjorn Jensen,ou=Accounting,dc=airius,dc=com
cn: Bjorn Jensen
objectclass: organizationalPerson
objectclass: person
objectclass: top
sn: Jensen
telephonenumber: +1 408 555 1212
EOL
  end

  def test_an_record
    ldif_source = <<-EOL
version: 1
dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Barbara Jensen
cn: Barbara J Jensen
cn: Babs Jensen
sn: Jensen
uid: bjensen
telephonenumber: +1 408 555 1212
description: A big sailing fan.
EOL

    record = {
      "dn" => "cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com",
      "objectclass" => ["top", "person", "organizationalPerson"],
      "cn" => ["Barbara Jensen", "Barbara J Jensen", "Babs Jensen"],
      "sn" => ["Jensen"],
      "uid" => ["bjensen"],
      "telephonenumber" => ["+1 408 555 1212"],
      "description" => ["A big sailing fan."],
    }
    assert_ldif(1, [record], ldif_source)
  end

  def test_an_record_to_s
    ldif_source = <<-EOL
version: 1
dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
cn: Barbara Jensen
cn: Barbara J Jensen
cn: Babs Jensen
sn: Jensen
uid: bjensen
telephonenumber: +1 408 555 1212
description: A big sailing fan.
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com
cn: Babs Jensen
cn: Barbara J Jensen
cn: Barbara Jensen
description: A big sailing fan.
objectclass: organizationalPerson
objectclass: person
objectclass: top
sn: Jensen
telephonenumber: +1 408 555 1212
uid: bjensen
EOL
  end

  def test_comment
    ldif_source = <<-EOL
version: 1
dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
objectclass: top
# objectclass: person
#objectcl
 ass: organizationalPerson
EOL

    record = {
      "dn" => "cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com",
      "objectclass" => ["top"],
    }
    assert_ldif(1, [record], ldif_source)
  end

  def test_comment_to_s
    ldif_source = <<-EOL
version: 1
dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
objectclass: top
# objectclass: person
#objectcl
 ass: organizationalPerson
EOL

    assert_ldif_to_s(<<-EOL, ldif_source)
version: 1
dn: cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com
objectclass: top
EOL
  end

  def test_dn_spec
    assert_invalid_ldif("'dn:' is missing",
                        "version: 1\n", 2, 1, "version: 1\n|@|")
    assert_invalid_ldif("DN is missing",
                        "version: 1\ndn:", 2, 4, "dn:|@|")
    assert_invalid_ldif("DN is missing",
                        "version: 1\ndn::", 2, 5, "dn::|@|")
    assert_invalid_ldif("DN is missing",
                        "version: 1\ndn:\n", 2, 4, "dn:|@|\n")
    assert_invalid_ldif("DN is missing",
                        "version: 1\ndn: \n", 2, 5, "dn: |@|\n")

    dn = "cn=Barbara Jensen,ou=Product Development,dc=example,dc=com"
    cn = "Barbara Jensen"
    assert_valid_dn(dn, "version: 1\ndn: #{dn}\ncn:#{cn}\n")

    encoded_dn = Base64.encode64(dn).gsub(/\n/, "\n ")
    encoded_cn = Base64.encode64(cn).gsub(/\n/, "\n ")
    assert_valid_dn(dn, "version: 1\ndn:: #{encoded_dn}\ncn::#{encoded_cn}\n")
  end

  def test_version_number
    assert_valid_version(1, "version: 1\ndn: dc=com\ndc: com")
    assert_valid_version(1, "version: 1\r\ndn: dc=com\ndc: com\n")
    assert_valid_version(1, "version: 1\r\n\n\r\n\ndn: dc=com\ndc: com\n")

    assert_invalid_ldif(["unsupported version: %d", 0],
                        "version: 0", 1, 11, "version: 0|@|")
    assert_invalid_ldif(["unsupported version: %d", 2],
                        "version: 2", 1, 11, "version: 2|@|")

    assert_invalid_ldif("separator is missing",
                        "version: 1", 1, 11, "version: 1|@|")
  end

  def test_version_spec
    assert_invalid_ldif("version spec is missing",
                        "", 1, 1, "|@|")
    assert_invalid_ldif("version spec is missing",
                        "VERSION: 1", 1, 1, "|@|VERSION: 1")
    assert_invalid_ldif("version number is missing",
                        "version:", 1, 9, "version:|@|")
    assert_invalid_ldif("version number is missing",
                        "version: ", 1, 10, "version: |@|")
    assert_invalid_ldif("version number is missing",
                        "version: XXX", 1, 10, "version: |@|XXX")
  end

  private
  def assert_ldif(version, records, ldif_source)
    encoding = ldif_source.encoding if ldif_source.respond_to?(:encoding)
    ldif = ActiveLdap::Ldif.parse(ldif_source)
    assert_equal(version, ldif.version)
    assert_equal(records,
                 ldif.records.collect {|record| record.to_hash})

    regenerated_ldif = ldif.to_s
    set_encoding(regenerated_ldif, encoding)
    reparsed_ldif = ActiveLdap::Ldif.parse(regenerated_ldif)
    assert_equal(ldif, reparsed_ldif)

    ldif
  end

  def assert_valid_dn(dn, ldif_source)
    ldif = ActiveLdap::Ldif.parse(ldif_source)
    assert_equal([dn], ldif.records.collect {|record| record.dn})
  end

  def assert_valid_version(version, ldif_source)
    ldif = ActiveLdap::Ldif.parse(ldif_source)
    assert_equal(version, ldif.version)
  end

  def assert_invalid_ldif(reason, ldif, line, column, nearest)
    exception = assert_raise(ActiveLdap::LdifInvalid) do
      ActiveLdap::Ldif.parse(ldif)
    end
    reason, *params = reason
    params = params.collect {|param| param.is_a?(String) ? _(param) : param}
    assert_equal([_(reason) % params, line, column, nearest, ldif],
                 [exception.reason, exception.line, exception.column,
                  exception.nearest, exception.ldif])
  end

  def assert_ldif_to_s(expected_ldif_source, original_ldif_source)
    ldif = ActiveLdap::Ldif.parse(original_ldif_source)
    assert_equal(expected_ldif_source, ldif.to_s)
  end

  def set_encoding(string, encoding)
    string.force_encoding("utf-8") if string.respond_to?(:force_encoding)
  end
end
