[DRE-commits] [ruby-net-ldap] 01/07: Imported Upstream version 0.11

Sebastien Badia sbadia-guest at moszumanska.debian.org
Fri Apr 10 15:47:27 UTC 2015


This is an automated email from the git hooks/post-receive script.

sbadia-guest pushed a commit to branch master
in repository ruby-net-ldap.

commit 94f2f4c10a84ecd98b04b8d81b6c0a7f2d74e12f
Author: Sebastien Badia <seb at sebian.fr>
Date:   Fri Apr 10 17:09:37 2015 +0200

    Imported Upstream version 0.11
---
 .autotest                              |  11 -
 .gemtest                               |   0
 .gitignore                             |   9 +
 .rspec                                 |   2 -
 .rubocop.yml                           |   5 +
 .rubocop_todo.yml                      | 462 +++++++++++++++++
 .travis.yml                            |  20 +-
 CONTRIBUTING.md                        |  54 ++
 Contributors.rdoc                      |   2 +
 Hacking.rdoc                           |   8 +-
 History.rdoc                           |  62 +++
 Manifest.txt                           |  56 --
 README.rdoc                            |  25 +-
 Rakefile                               |  84 +--
 autotest/discover.rb                   |   1 -
 lib/net/ber/ber_parser.rb              |  15 +-
 lib/net/ber/core_ext.rb                |  19 +-
 lib/net/ber/core_ext/bignum.rb         |  22 -
 lib/net/ber/core_ext/fixnum.rb         |  66 ---
 lib/net/ber/core_ext/integer.rb        |  74 +++
 lib/net/ber/core_ext/string.rb         |  14 +-
 lib/net/ber/core_ext/true_class.rb     |   5 +-
 lib/net/ldap.rb                        | 899 +++++++--------------------------
 lib/net/ldap/connection.rb             | 702 +++++++++++++++++++++++++
 lib/net/ldap/dataset.rb                |  22 +-
 lib/net/ldap/entry.rb                  |   4 +-
 lib/net/ldap/error.rb                  |  38 ++
 lib/net/ldap/filter.rb                 |  35 +-
 lib/net/ldap/password.rb               |  22 +-
 lib/net/ldap/pdu.rb                    |  32 +-
 lib/net/ldap/version.rb                |   2 +-
 lib/net/snmp.rb                        | 476 +++++++++--------
 metadata.yml                           | 169 +++----
 net-ldap.gemspec                       |  42 +-
 script/install-openldap                | 111 ++++
 script/package                         |   7 +
 script/release                         |  16 +
 spec/integration/ssl_ber_spec.rb       |  39 --
 spec/spec.opts                         |   2 -
 spec/spec_helper.rb                    |  28 -
 spec/unit/ber/ber_spec.rb              | 141 ------
 spec/unit/ber/core_ext/array_spec.rb   |  24 -
 spec/unit/ber/core_ext/string_spec.rb  |  51 --
 spec/unit/ldap/dn_spec.rb              |  80 ---
 spec/unit/ldap/entry_spec.rb           |  51 --
 spec/unit/ldap/filter_parser_spec.rb   |  26 -
 spec/unit/ldap/filter_spec.rb          | 115 -----
 spec/unit/ldap/search_spec.rb          |  49 --
 spec/unit/ldap_spec.rb                 | 223 --------
 test/ber/core_ext/test_array.rb        |  22 +
 test/ber/core_ext/test_string.rb       |  25 +
 test/ber/test_ber.rb                   | 145 ++++++
 test/common.rb                         |   3 -
 test/fixtures/cacert.pem               |  20 +
 test/fixtures/openldap/memberof.ldif   |  33 ++
 test/fixtures/openldap/retcode.ldif    |  76 +++
 test/fixtures/openldap/slapd.conf.ldif |  67 +++
 test/fixtures/seed.ldif                | 374 ++++++++++++++
 test/integration/test_add.rb           |  28 +
 test/integration/test_ber.rb           |  30 ++
 test/integration/test_bind.rb          |  34 ++
 test/integration/test_delete.rb        |  31 ++
 test/integration/test_open.rb          |  88 ++++
 test/integration/test_return_codes.rb  |  38 ++
 test/integration/test_search.rb        |  77 +++
 test/support/vm/openldap/.gitignore    |   1 +
 test/support/vm/openldap/README.md     |  32 ++
 test/support/vm/openldap/Vagrantfile   |  33 ++
 test/test_dn.rb                        |  44 ++
 test/test_entry.rb                     | 118 +++--
 test/test_filter.rb                    | 109 +++-
 test/test_filter_parser.rb             |  20 +
 test/test_helper.rb                    |  66 +++
 test/test_ldap.rb                      |  60 +++
 test/test_ldap_connection.rb           | 384 +++++++++++++-
 test/test_ldif.rb                      |  27 +-
 test/test_password.rb                  |  13 +-
 test/test_rename.rb                    |   4 +-
 test/test_search.rb                    |  39 ++
 test/test_snmp.rb                      |   2 +-
 test/test_ssl_ber.rb                   |  40 ++
 81 files changed, 4218 insertions(+), 2287 deletions(-)

diff --git a/.autotest b/.autotest
deleted file mode 100644
index f5f85be..0000000
--- a/.autotest
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'rubygems'
-#require 'redgreen/autotest'
-require 'autotest/timestamp'
-
-Autotest.add_hook :initialize do |autotest|
-  %w{.git .hg .DS_Store ._* tmp log doc}.each do |exception|
-    autotest.add_exception(exception)
-  end
-end
-
-# vim: syntax=ruby
diff --git a/.gemtest b/.gemtest
deleted file mode 100644
index e69de29..0000000
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9c2842d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+*~
+*.swp
+.rvmrc
+pkg/
+doc/
+publish/
+Gemfile.lock
+.bundle
+bin/
diff --git a/.rspec b/.rspec
deleted file mode 100644
index 7438fbe..0000000
--- a/.rspec
+++ /dev/null
@@ -1,2 +0,0 @@
---colour
---format documentation
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..85ffa20
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,5 @@
+inherit_from: .rubocop_todo.yml
+
+AllCops:
+  Exclude:
+    - 'pkg/**/*'
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
new file mode 100644
index 0000000..5a5dcbc
--- /dev/null
+++ b/.rubocop_todo.yml
@@ -0,0 +1,462 @@
+# This configuration was generated by `rubocop --auto-gen-config`
+# on 2014-12-19 15:32:44 +1100 using RuboCop version 0.28.0.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
+
+# Offense count: 12
+# Configuration parameters: AllowSafeAssignment.
+Lint/AssignmentInCondition:
+  Enabled: false
+
+# Offense count: 1
+# Configuration parameters: AlignWith, SupportedStyles.
+Lint/EndAlignment:
+  Enabled: false
+
+# Offense count: 1
+Lint/RescueException:
+  Enabled: false
+
+# Offense count: 1
+Lint/ShadowingOuterLocalVariable:
+  Enabled: false
+
+# Offense count: 9
+# Cop supports --auto-correct.
+Lint/UnusedBlockArgument:
+  Enabled: false
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Lint/UnusedMethodArgument:
+  Enabled: false
+
+# Offense count: 7
+Lint/UselessAssignment:
+  Enabled: false
+
+# Offense count: 47
+Metrics/AbcSize:
+  Max: 114
+
+# Offense count: 11
+Metrics/BlockNesting:
+  Max: 4
+
+# Offense count: 9
+# Configuration parameters: CountComments.
+Metrics/ClassLength:
+  Max: 470
+
+# Offense count: 20
+Metrics/CyclomaticComplexity:
+  Max: 41
+
+# Offense count: 193
+# Configuration parameters: AllowURI, URISchemes.
+Metrics/LineLength:
+  Max: 360
+
+# Offense count: 71
+# Configuration parameters: CountComments.
+Metrics/MethodLength:
+  Max: 130
+
+# Offense count: 13
+Metrics/PerceivedComplexity:
+  Max: 36
+
+# Offense count: 1
+Style/AccessorMethodName:
+  Enabled: false
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/AlignArray:
+  Enabled: false
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/AlignParameters:
+  Enabled: false
+
+# Offense count: 36
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/AndOr:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/BarePercentLiterals:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/BlockComments:
+  Enabled: false
+
+# Offense count: 20
+# Cop supports --auto-correct.
+Style/Blocks:
+  Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/BracesAroundHashParameters:
+  Enabled: false
+
+# Offense count: 4
+# Configuration parameters: IndentWhenRelativeTo, SupportedStyles, IndentOneStep.
+Style/CaseIndentation:
+  Enabled: false
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/CharacterLiteral:
+  Enabled: false
+
+# Offense count: 22
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/ClassAndModuleChildren:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/ClassCheck:
+  Enabled: false
+
+# Offense count: 13
+# Cop supports --auto-correct.
+Style/ColonMethodCall:
+  Enabled: false
+
+# Offense count: 2
+# Configuration parameters: Keywords.
+Style/CommentAnnotation:
+  Enabled: false
+
+# Offense count: 86
+Style/ConstantName:
+  Enabled: false
+
+# Offense count: 18
+# Cop supports --auto-correct.
+Style/DeprecatedHashMethods:
+  Enabled: false
+
+# Offense count: 46
+Style/Documentation:
+  Enabled: false
+
+# Offense count: 23
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/DotPosition:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/ElseAlignment:
+  Enabled: false
+
+# Offense count: 4
+# Cop supports --auto-correct.
+# Configuration parameters: AllowAdjacentOneLineDefs.
+Style/EmptyLineBetweenDefs:
+  Enabled: false
+
+# Offense count: 9
+# Cop supports --auto-correct.
+Style/EmptyLines:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/EmptyLinesAroundClassBody:
+  Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/EmptyLinesAroundModuleBody:
+  Enabled: false
+
+# Offense count: 3
+Style/EvenOdd:
+  Enabled: false
+
+# Offense count: 1
+# Configuration parameters: Exclude.
+Style/FileName:
+  Enabled: false
+
+# Offense count: 9
+# Configuration parameters: AllowedVariables.
+Style/GlobalVars:
+  Enabled: false
+
+# Offense count: 3
+# Configuration parameters: MinBodyLength.
+Style/GuardClause:
+  Enabled: false
+
+# Offense count: 150
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/HashSyntax:
+  Enabled: false
+
+# Offense count: 8
+# Configuration parameters: MaxLineLength.
+Style/IfUnlessModifier:
+  Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/IndentHash:
+  Enabled: false
+
+# Offense count: 6
+# Cop supports --auto-correct.
+# Configuration parameters: Width.
+Style/IndentationWidth:
+  Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/LeadingCommentSpace:
+  Enabled: false
+
+# Offense count: 21
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/MethodDefParentheses:
+  Enabled: false
+
+# Offense count: 1
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/MethodName:
+  Enabled: false
+
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/MultilineOperationIndentation:
+  Enabled: false
+
+# Offense count: 1
+Style/MultilineTernaryOperator:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/NegatedIf:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/NegatedWhile:
+  Enabled: false
+
+# Offense count: 3
+# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
+Style/Next:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/NilComparison:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: IncludeSemanticChanges.
+Style/NonNilCheck:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/Not:
+  Enabled: false
+
+# Offense count: 10
+# Cop supports --auto-correct.
+Style/NumericLiterals:
+  MinDigits: 8
+
+# Offense count: 3
+Style/OpMethod:
+  Enabled: false
+
+# Offense count: 6
+# Cop supports --auto-correct.
+# Configuration parameters: AllowSafeAssignment.
+Style/ParenthesesAroundCondition:
+  Enabled: false
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: PreferredDelimiters.
+Style/PercentLiteralDelimiters:
+  Enabled: false
+
+# Offense count: 11
+# Cop supports --auto-correct.
+Style/PerlBackrefs:
+  Enabled: false
+
+# Offense count: 9
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/RaiseArgs:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/RedundantBegin:
+  Enabled: false
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: AllowMultipleReturnValues.
+Style/RedundantReturn:
+  Enabled: false
+
+# Offense count: 7
+# Cop supports --auto-correct.
+Style/RedundantSelf:
+  Enabled: false
+
+# Offense count: 1
+# Configuration parameters: MaxSlashes.
+Style/RegexpLiteral:
+  Enabled: false
+
+# Offense count: 2
+Style/RescueModifier:
+  Enabled: false
+
+# Offense count: 7
+# Cop supports --auto-correct.
+# Configuration parameters: AllowAsExpressionSeparator.
+Style/Semicolon:
+  Enabled: false
+
+# Offense count: 61
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/SignalException:
+  Enabled: false
+
+# Offense count: 2
+# Configuration parameters: Methods.
+Style/SingleLineBlockParams:
+  Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/SingleSpaceBeforeFirstArg:
+  Enabled: false
+
+# Offense count: 24
+# Cop supports --auto-correct.
+Style/SpaceAfterComma:
+  Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/SpaceAroundEqualsInParameterDefault:
+  Enabled: false
+
+# Offense count: 8
+# Cop supports --auto-correct.
+Style/SpaceAroundOperators:
+  Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/SpaceBeforeBlockBraces:
+  Enabled: false
+
+# Offense count: 18
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
+Style/SpaceInsideBlockBraces:
+  Enabled: false
+
+# Offense count: 37
+# Cop supports --auto-correct.
+Style/SpaceInsideBrackets:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SupportedStyles.
+Style/SpaceInsideHashLiteralBraces:
+  Enabled: false
+
+# Offense count: 20
+# Cop supports --auto-correct.
+Style/SpaceInsideParens:
+  Enabled: false
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/SpecialGlobalVars:
+  Enabled: false
+
+# Offense count: 645
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/StringLiterals:
+  Enabled: false
+
+# Offense count: 10
+# Cop supports --auto-correct.
+# Configuration parameters: IgnoredMethods.
+Style/SymbolProc:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/TrailingBlankLines:
+  Enabled: false
+
+# Offense count: 9
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
+Style/TrailingComma:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, Whitelist.
+Style/TrivialAccessors:
+  Enabled: false
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/UnneededPercentQ:
+  Enabled: false
+
+# Offense count: 1
+# Configuration parameters: MaxLineLength.
+Style/WhileUntilModifier:
+  Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: WordRegex.
+Style/WordArray:
+  MinSize: 2
diff --git a/.travis.yml b/.travis.yml
index 6877e70..311ed4c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,9 +2,27 @@ language: ruby
 rvm:
   - 1.9.3
   - 2.0.0
+  - 2.1.2
+  # optional
   - jruby-19mode
   - rbx-19mode
+  - rbx-2
+
+env:
+  - INTEGRATION=openldap
+
+install:
+  - if [ "$INTEGRATION" = "openldap" ]; then ./script/install-openldap; fi
+  - bundle install
+
+script: bundle exec rake ci
+
 matrix:
   allow_failures:
     - rvm: jruby-19mode
-script: bundle exec rake spec
+    - rvm: rbx-19mode
+    - rvm: rbx-2
+  fast_finish: true
+
+notifications:
+  email: false
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0247a3d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,54 @@
+# Contribution guide
+
+Thank you for using net-ldap. If you'd like to help, keep these guidelines in
+mind.
+
+## Submitting a New Issue
+
+If you find a bug, or would like to propose an idea, file a [new issue][issues].
+Include as many details as possible:
+
+- Version of net-ldap gem
+- LDAP server version
+- Queries, connection information, any other input
+- output or error messages
+
+## Sending a Pull Request
+
+[Pull requests][pr] are always welcome!
+
+Check out [the project's issues list][issues] for ideas on what could be improved.
+
+Before sending, please add tests and ensure the test suite passes.
+
+To run the full suite:
+
+  `bundle exec rake`
+
+To run a specific test file:
+
+  `bundle exec ruby test/test_ldap.rb`
+
+To run a specific test:
+
+  `bundle exec ruby test/test_ldap.rb -n test_instrument_bind`
+
+Pull requests will trigger automatic continuous integration builds on
+[TravisCI][travis]. To run integration tests locally, see the `test/support`
+folder.
+
+## Styleguide
+
+```ruby
+# 1.9+ style hashes
+{key: "value"}
+
+# Multi-line arguments with `\`
+MyClass.new \
+  foo: 'bar',
+  baz: 'garply'
+```
+
+[issues]: https://github.com/ruby-net-ldap/ruby-net-ldap/issues
+[pr]: https://help.github.com/articles/using-pull-requests
+[travis]: https://travis-ci.org/ruby-ldap/ruby-net-ldap
diff --git a/Contributors.rdoc b/Contributors.rdoc
index bef012a..e40b20d 100644
--- a/Contributors.rdoc
+++ b/Contributors.rdoc
@@ -20,3 +20,5 @@ Contributions since:
 * Erik Hetzner (egh)
 * nowhereman
 * David J. Lee (DavidJLee)
+* Cody Cutrer (ccutrer)
+* WoodsBagotAndreMarquesLee
diff --git a/Hacking.rdoc b/Hacking.rdoc
index 1db37cb..4bbf4ec 100644
--- a/Hacking.rdoc
+++ b/Hacking.rdoc
@@ -40,8 +40,8 @@ modification to +Contributors.rdoc+ to add yourself.
 
 == Tests
 
-The Net::LDAP team uses RSpec for unit testing; all changes must have rspec
-tests for any new or changed features.
+The Net::LDAP team uses [Minitest](http://docs.seattlerb.org/minitest/) for unit
+testing; all changes must have tests for any new or changed features.
 
 Your changes should have been tested against at least one real LDAP server; the
 current tests are not sufficient to find all possible bugs. It's unlikely that
@@ -55,10 +55,6 @@ us with a sample LDIF data file for importing into LDAP servers for testing.
 Net::LDAP uses several libraries during development, all of which can be
 installed using RubyGems.
 
-* *hoe*
-* *hoe-git*
-* *metaid*
-* *rspec*
 * *flexmock*
 
 == Participation
diff --git a/History.rdoc b/History.rdoc
index e0ee3e0..fa7ff5a 100644
--- a/History.rdoc
+++ b/History.rdoc
@@ -1,3 +1,65 @@
+=== Net::LDAP 0.11
+* Major enhancements:
+  * #183 Specific errors subclassing Net::LDAP::Error
+* Bug fixes:
+  * #176 Fix nil tls options
+  * #184 Search guards against nil queued reads. Connection#unescape handles numerics
+* Code clean-up:
+  * #180 Refactor connection establishment
+
+=== Net::LDAP 0.10.1
+* Bug fixes:
+  * Fix Integer BER encoding of signed values
+
+=== Net::LDAP 0.10.0
+* Major enhancements:
+  * Accept SimpleTLS/StartTLS encryption options (compatible with `OpenSSL::SSL::SSLContext#set_params`)
+* Bug fixes:
+  * Parse filter strings with square and curly braces (`[]` and `{}`)
+  * Handle connection timeout errors (`Errno::ETIMEDOUT` raised as `Net::LDAP::LdapError`)
+* Testing changes:
+  * Add integration tests for StartTLS connections to OpenLDAP
+* Meta changes:
+  * Update Gem release tooling (remove Hoe, use Rake)
+  * Fix Gem release date
+
+=== Net::LDAP 0.9.0
+* Major changes:
+  * Dropped support for ruby 1.8.7, ruby >= 1.9.3 now required
+* Major enhancements:
+  * Add support for search time limit parameter
+  * Instrument received messages, PDU parsing
+* Minor enhancments:
+  * Add support for querying ActiveDirectory capabilities from root dse
+* Bug fixes:
+  * Fix reads for multiple concurrent requests with shared, open connections mixing up the results
+  * Fix search size option
+  * Fix BER encoding bug
+* Code clean-up:
+  * Added integration test suite
+  * Switch to minitest
+
+* Details
+  * #150 Support querying ActiveDirectory capabilities when searching root dse
+  * #142 Encode true as xFF
+  * #124, #145, #146, #152 Cleanup gemspec
+  * #138, #144 Track response messages by message id
+  * #141 Magic number/constant cleanup
+  * #119, #129, #130, #132, #133, #137 Integration tests
+  * #115 Search timeout support
+  * #140 Fix search size option
+  * #139 Cleanup and inline documentation for Net::LDAP::Connection#search
+  * #131 Instrumentation
+  * #116 Refactor Connection#write
+  * #126 Update gitignore
+  * #128 Fix whitespace
+  * #113, #121 Switch to minitest
+  * #123 Base64 encoded dn
+  * #114 Separate file for Net::LDAP::Connection
+  * #104 Parse version spec in LDIF datasets
+  * #106 ldap.modify doc fixes
+  * #111 Fix test deprecations
+
 === Net::LDAP 0.5.0 / 2013-07-22
 * Major changes:
   * Required Ruby version is >=1.9.3
diff --git a/Manifest.txt b/Manifest.txt
deleted file mode 100644
index bea51c0..0000000
--- a/Manifest.txt
+++ /dev/null
@@ -1,56 +0,0 @@
-.autotest
-.rspec
-.travis.yml
-Contributors.rdoc
-Gemfile
-Hacking.rdoc
-History.rdoc
-License.rdoc
-Manifest.txt
-README.rdoc
-Rakefile
-autotest/discover.rb
-lib/net-ldap.rb
-lib/net/ber.rb
-lib/net/ber/ber_parser.rb
-lib/net/ber/core_ext.rb
-lib/net/ber/core_ext/array.rb
-lib/net/ber/core_ext/bignum.rb
-lib/net/ber/core_ext/false_class.rb
-lib/net/ber/core_ext/fixnum.rb
-lib/net/ber/core_ext/string.rb
-lib/net/ber/core_ext/true_class.rb
-lib/net/ldap.rb
-lib/net/ldap/dataset.rb
-lib/net/ldap/dn.rb
-lib/net/ldap/entry.rb
-lib/net/ldap/filter.rb
-lib/net/ldap/instrumentation.rb
-lib/net/ldap/password.rb
-lib/net/ldap/pdu.rb
-lib/net/ldap/version.rb
-lib/net/snmp.rb
-net-ldap.gemspec
-spec/integration/ssl_ber_spec.rb
-spec/spec.opts
-spec/spec_helper.rb
-spec/unit/ber/ber_spec.rb
-spec/unit/ber/core_ext/array_spec.rb
-spec/unit/ber/core_ext/string_spec.rb
-spec/unit/ldap/dn_spec.rb
-spec/unit/ldap/entry_spec.rb
-spec/unit/ldap/filter_parser_spec.rb
-spec/unit/ldap/filter_spec.rb
-spec/unit/ldap/search_spec.rb
-spec/unit/ldap_spec.rb
-test/common.rb
-test/test_entry.rb
-test/test_filter.rb
-test/test_ldap_connection.rb
-test/test_ldif.rb
-test/test_password.rb
-test/test_rename.rb
-test/test_snmp.rb
-test/testdata.ldif
-testserver/ldapserver.rb
-testserver/testdata.ldif
diff --git a/README.rdoc b/README.rdoc
index 969a717..b8331f7 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -1,4 +1,4 @@
-= Net::LDAP for Ruby {<img src="https://travis-ci.org/github/ruby-net-ldap.png" />}[https://travis-ci.org/github/ruby-net-ldap]
+= Net::LDAP for Ruby {<img src="https://travis-ci.org/ruby-ldap/ruby-net-ldap.png" />}[https://travis-ci.org/github/ruby-net-ldap]
 
 == Description
 
@@ -37,6 +37,29 @@ sources.
 
 Simply require either 'net-ldap' or 'net/ldap'.
 
+== Develop
+
+This task will run the test suite and the
+{RuboCop}[https://github.com/bbatsov/rubocop] static code analyzer.
+
+  rake rubotest
+
+To run the integration tests against an LDAP server:
+
+  cd test/support/vm/openldap
+  vagrant up
+  cd ../../../..
+  INTEGRATION=openldap bundle exec rake rubotest
+
+== Release
+
+This section is for gem maintainers to cut a new version of the gem.
+
+* Update lib/net/ldap/version.rb to next version number X.X.X following {semver}(http://semver.org/).
+* Update `History.rdoc`. Get latest changes with `git log --oneline vLAST_RELEASE..HEAD | grep Merge`
+
+* On the master branch, run `script/release`
+
 :include: Contributors.rdoc
 
 :include: License.rdoc
diff --git a/Rakefile b/Rakefile
index 159a8a0..76ec8c0 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,76 +1,24 @@
+#!/usr/bin/env rake
 # -*- ruby encoding: utf-8 -*-
+# vim: syntax=ruby
 
-require "rubygems"
-require 'hoe'
-
-Hoe.plugin :doofus
-Hoe.plugin :git
-Hoe.plugin :gemspec
-
-Hoe.spec 'net-ldap' do |spec|
-  # spec.rubyforge_name = spec.name
-
-  spec.developer("Francis Cianfrocca", "blackhedd at rubyforge.org")
-  spec.developer("Emiel van de Laar", "gemiel at gmail.com")
-  spec.developer("Rory O'Connell", "rory.ocon at gmail.com")
-  spec.developer("Kaspar Schiess", "kaspar.schiess at absurd.li")
-  spec.developer("Austin Ziegler", "austin at rubyforge.org")
-  spec.developer("Michael Schaarschmidt", "michael at schaaryworks.com")
-
-  spec.remote_rdoc_dir = ''
-  spec.rsync_args << ' --exclude=statsvn/'
-
-  spec.urls = %w(http://rubyldap.com/' 'https://github.com/ruby-ldap/ruby-net-ldap)
-  spec.licenses = ['MIT']
-
-  spec.history_file = 'History.rdoc'
-  spec.readme_file = 'README.rdoc'
-
-  spec.extra_rdoc_files = FileList["*.rdoc"].to_a
-
-  spec.extra_dev_deps << [ "hoe-git", "~> 1" ]
-  spec.extra_dev_deps << [ "hoe-gemspec", "~> 1" ]
-  spec.extra_dev_deps << [ "metaid", "~> 1" ]
-  spec.extra_dev_deps << [ "flexmock", ">= 1.3.0" ]
-  spec.extra_dev_deps << [ "rspec", "~> 2.0" ]
-
-  spec.clean_globs << "coverage"
+require 'rake/testtask'
+require 'rubocop/rake_task'
+require 'bundler'
 
-  spec.spec_extras[:required_ruby_version] = ">= 1.8.7"
-  spec.multiruby_skip << "1.8.6"
-  spec.multiruby_skip << "1_8_6"
+RuboCop::RakeTask.new
 
-  spec.need_tar = true
+Rake::TestTask.new do |t|
+  t.libs << 'test'
+  t.test_files = FileList['test/**/test_*.rb']
+  t.verbose = true
+  t.description = 'Run tests, set INTEGRATION=openldap to run integration tests, INTEGRATION_HOST and INTEGRATION_PORT are also supported'
 end
 
-# I'm not quite ready to get rid of this, but I think "rake git:manifest" is
-# sufficient.
-namespace :old do
-  desc "Build the manifest file from the current set of files."
-  task :build_manifest do |t|
-    require 'find'
+desc 'Run tests and RuboCop (RuboCop runs on mri only)'
+task ci: [:test]
 
-    paths = []
-    Find.find(".") do |path|
-      next if File.directory?(path)
-      next if path =~ /\.svn/
-        next if path =~ /\.git/
-        next if path =~ /\.hoerc/
-        next if path =~ /\.swp$/
-        next if path =~ %r{coverage/}
-      next if path =~ /~$/
-        paths << path.sub(%r{^\./}, '')
-    end
+desc 'Run tests and RuboCop'
+task rubotest: [:test, :rubocop]
 
-    File.open("Manifest.txt", "w") do |f|
-      f.puts paths.sort.join("\n")
-    end
-
-    puts paths.sort.join("\n")
-  end
-end
-
-desc "Run a full set of integration and unit tests" 
-task :cruise => [:test, :spec]
-
-# vim: syntax=ruby
+task default: Bundler.current_ruby.mri? ? [:test, :rubocop] : [:test]
diff --git a/autotest/discover.rb b/autotest/discover.rb
deleted file mode 100644
index cd6892c..0000000
--- a/autotest/discover.rb
+++ /dev/null
@@ -1 +0,0 @@
-Autotest.add_discovery { "rspec2" }
diff --git a/lib/net/ber/ber_parser.rb b/lib/net/ber/ber_parser.rb
index ea6c078..09de8c8 100644
--- a/lib/net/ber/ber_parser.rb
+++ b/lib/net/ber/ber_parser.rb
@@ -41,9 +41,18 @@ module Net::BER::BERParser
       s.ber_identifier = id
       s
     elsif object_type == :integer
-      j = 0
-      data.each_byte { |b| j = (j << 8) + b }
-      j
+      neg = !(data.unpack("C").first & 0x80).zero?
+      int = 0
+
+      data.each_byte do |b|
+        int = (int << 8) + (neg ? 255 - b : b)
+      end
+
+      if neg
+        (int + 1) * -1
+      else
+        int
+      end
     elsif object_type == :oid
       # See X.690 pgh 8.19 for an explanation of this algorithm.
       # This is potentially not good enough. We may need a
diff --git a/lib/net/ber/core_ext.rb b/lib/net/ber/core_ext.rb
index b176df7..b193984 100644
--- a/lib/net/ber/core_ext.rb
+++ b/lib/net/ber/core_ext.rb
@@ -28,35 +28,28 @@ end
 
 require 'net/ber/core_ext/array'
 # :stopdoc:
-class Array 
+class Array
   include Net::BER::Extensions::Array
 end
 # :startdoc:
 
-require 'net/ber/core_ext/bignum'
+require 'net/ber/core_ext/integer'
 # :stopdoc:
-class Bignum 
-  include Net::BER::Extensions::Bignum
-end
-# :startdoc:
-
-require 'net/ber/core_ext/fixnum'
-# :stopdoc:
-class Fixnum 
-  include Net::BER::Extensions::Fixnum
+class Integer
+  include Net::BER::Extensions::Integer
 end
 # :startdoc:
 
 require 'net/ber/core_ext/true_class'
 # :stopdoc:
-class TrueClass 
+class TrueClass
   include Net::BER::Extensions::TrueClass
 end
 # :startdoc:
 
 require 'net/ber/core_ext/false_class'
 # :stopdoc:
-class FalseClass 
+class FalseClass
   include Net::BER::Extensions::FalseClass
 end
 # :startdoc:
diff --git a/lib/net/ber/core_ext/bignum.rb b/lib/net/ber/core_ext/bignum.rb
deleted file mode 100644
index dc62fb8..0000000
--- a/lib/net/ber/core_ext/bignum.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- ruby encoding: utf-8 -*-
-##
-# BER extensions to the Bignum class.
-module Net::BER::Extensions::Bignum
-  ##
-  # Converts a Bignum to an uncompressed BER integer.
-  def to_ber
-    result = []
-
-    # NOTE: Array#pack's 'w' is a BER _compressed_ integer. We need
-    # uncompressed BER integers, so we're not using that. See also:
-    # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/228864
-    n = self
-    while n > 0
-      b = n & 0xff
-      result << b
-      n = n >> 8
-    end
-
-    "\002" + ([result.size] + result.reverse).pack('C*')
-  end
-end
diff --git a/lib/net/ber/core_ext/fixnum.rb b/lib/net/ber/core_ext/fixnum.rb
deleted file mode 100644
index 221badd..0000000
--- a/lib/net/ber/core_ext/fixnum.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# -*- ruby encoding: utf-8 -*-
-##
-# Ber extensions to the Fixnum class.
-module Net::BER::Extensions::Fixnum
-  ##
-  # Converts the fixnum to BER format.
-  def to_ber
-    "\002#{to_ber_internal}"
-  end
-
-  ##
-  # Converts the fixnum to BER enumerated format.
-  def to_ber_enumerated
-    "\012#{to_ber_internal}"
-  end
-
-  ##
-  # Converts the fixnum to BER length encodining format.
-  def to_ber_length_encoding
-    if self <= 127
-      [self].pack('C')
-    else
-      i = [self].pack('N').sub(/^[\0]+/,"")
-      [0x80 + i.length].pack('C') + i
-    end
-  end
-
-  ##
-  # Generate a BER-encoding for an application-defined INTEGER. Examples of
-  # such integers are SNMP's Counter, Gauge, and TimeTick types.
-  def to_ber_application(tag)
-    [0x40 + tag].pack("C") + to_ber_internal
-  end
-
-  ##
-  # Used to BER-encode the length and content bytes of a Fixnum. Callers
-  # must prepend the tag byte for the contained value.
-  def to_ber_internal
-    # CAUTION: Bit twiddling ahead. You might want to shield your eyes or
-    # something.
-
-    # Looks for the first byte in the fixnum that is not all zeroes. It does
-    # this by masking one byte after another, checking the result for bits
-    # that are left on.
-    size = Net::BER::MAX_FIXNUM_SIZE
-    while size > 1
-      break if (self & (0xff << (size - 1) * 8)) > 0
-      size -= 1
-    end
-
-    # Store the size of the fixnum in the result
-    result = [size]
-
-    # Appends bytes to result, starting with higher orders first. Extraction
-    # of bytes is done by right shifting the original fixnum by an amount
-    # and then masking that with 0xff.
-    while size > 0
-      # right shift size - 1 bytes, mask with 0xff
-      result << ((self >> ((size - 1) * 8)) & 0xff)
-      size -= 1
-    end
-
-    result.pack('C*')
-  end
-  private :to_ber_internal
-end
diff --git a/lib/net/ber/core_ext/integer.rb b/lib/net/ber/core_ext/integer.rb
new file mode 100644
index 0000000..b2149f9
--- /dev/null
+++ b/lib/net/ber/core_ext/integer.rb
@@ -0,0 +1,74 @@
+# -*- ruby encoding: utf-8 -*-
+##
+# BER extensions to the Integer class, affecting Fixnum and Bignum objects.
+module Net::BER::Extensions::Integer
+  ##
+  # Converts the Integer to BER format.
+  def to_ber
+    "\002#{to_ber_internal}"
+  end
+
+  ##
+  # Converts the Integer to BER enumerated format.
+  def to_ber_enumerated
+    "\012#{to_ber_internal}"
+  end
+
+  ##
+  # Converts the Integer to BER length encoding format.
+  def to_ber_length_encoding
+    if self <= 127
+      [self].pack('C')
+    else
+      i = [self].pack('N').sub(/^[\0]+/,"")
+      [0x80 + i.length].pack('C') + i
+    end
+  end
+
+  ##
+  # Generate a BER-encoding for an application-defined INTEGER. Examples of
+  # such integers are SNMP's Counter, Gauge, and TimeTick types.
+  def to_ber_application(tag)
+    [0x40 + tag].pack("C") + to_ber_internal
+  end
+
+  ##
+  # Used to BER-encode the length and content bytes of an Integer. Callers
+  # must prepend the tag byte for the contained value.
+  def to_ber_internal
+    # Compute the byte length, accounting for negative values requiring two's
+    # complement.
+    size  = 1
+    size += 1 until (((self < 0) ? ~self : self) >> (size * 8)).zero?
+
+    # Padding for positive, negative values. See section 8.5 of ITU-T X.690:
+    # http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf
+
+    # For positive integers, if most significant bit in an octet is set to one,
+    # pad the result (otherwise it's decoded as a negative value).
+    if self > 0 && (self & (0x80 << (size - 1) * 8)) > 0
+      size += 1
+    end
+
+    # And for negative integers, pad if the most significant bit in the octet
+    # is not set to one (othwerise, it's decoded as positive value).
+    if self < 0 && (self & (0x80 << (size - 1) * 8)) == 0
+      size += 1
+    end
+
+    # Store the size of the Integer in the result
+    result = [size]
+
+    # Appends bytes to result, starting with higher orders first. Extraction
+    # of bytes is done by right shifting the original Integer by an amount
+    # and then masking that with 0xff.
+    while size > 0
+      # right shift size - 1 bytes, mask with 0xff
+      result << ((self >> ((size - 1) * 8)) & 0xff)
+      size -= 1
+    end
+
+    result.pack('C*')
+  end
+  private :to_ber_internal
+end
diff --git a/lib/net/ber/core_ext/string.rb b/lib/net/ber/core_ext/string.rb
index b4ad803..e8a43e2 100644
--- a/lib/net/ber/core_ext/string.rb
+++ b/lib/net/ber/core_ext/string.rb
@@ -16,13 +16,13 @@ module Net::BER::Extensions::String
     [code].pack('C') + raw_string.length.to_ber_length_encoding + raw_string
   end
 
-	##
-	# Converts a string to a BER string but does *not* encode to UTF-8 first.
-	# This is required for proper representation of binary data for Microsoft
-	# Active Directory
-	def to_ber_bin(code = 0x04)
-		[code].pack('C') + length.to_ber_length_encoding + self
-	end
+  ##
+  # Converts a string to a BER string but does *not* encode to UTF-8 first.
+  # This is required for proper representation of binary data for Microsoft
+  # Active Directory
+  def to_ber_bin(code = 0x04)
+    [code].pack('C') + length.to_ber_length_encoding + self
+  end
 
   def raw_utf8_encoded
     if self.respond_to?(:encode)
diff --git a/lib/net/ber/core_ext/true_class.rb b/lib/net/ber/core_ext/true_class.rb
index ac66c92..bf199d1 100644
--- a/lib/net/ber/core_ext/true_class.rb
+++ b/lib/net/ber/core_ext/true_class.rb
@@ -5,8 +5,7 @@ module Net::BER::Extensions::TrueClass
   ##
   # Converts +true+ to the BER wireline representation of +true+.
   def to_ber
-    # 20100319 AZ: Note that this may not be the completely correct value,
-    # per some test documentation. We need to determine the truth of this.
-    "\001\001\001"
+    # http://tools.ietf.org/html/rfc4511#section-5.1
+    "\001\001\xFF".force_encoding("ASCII-8BIT")
   end
 end
diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb
index 8201d8f..75b463f 100644
--- a/lib/net/ldap.rb
+++ b/lib/net/ldap.rb
@@ -24,7 +24,9 @@ require 'net/ldap/dataset'
 require 'net/ldap/password'
 require 'net/ldap/entry'
 require 'net/ldap/instrumentation'
+require 'net/ldap/connection'
 require 'net/ldap/version'
+require 'net/ldap/error'
 
 # == Quick-start for the Impatient
 # === Quick Example of a user-authentication against an LDAP directory:
@@ -245,8 +247,6 @@ require 'net/ldap/version'
 class Net::LDAP
   include Net::LDAP::Instrumentation
 
-  class LdapError < StandardError; end
-
   SearchScope_BaseObject = 0
   SearchScope_SingleLevel = 1
   SearchScope_WholeSubtree = 2
@@ -316,33 +316,107 @@ class Net::LDAP
   DefaultPort = 389
   DefaultAuth = { :method => :anonymous }
   DefaultTreebase = "dc=com"
-	DefaultForceNoPage = false
+  DefaultForceNoPage = false
 
   StartTlsOid = "1.3.6.1.4.1.1466.20037"
 
+  # https://tools.ietf.org/html/rfc4511#section-4.1.9
+  # https://tools.ietf.org/html/rfc4511#appendix-A
+  ResultCodeSuccess                      = 0
+  ResultCodeOperationsError              = 1
+  ResultCodeProtocolError                = 2
+  ResultCodeTimeLimitExceeded            = 3
+  ResultCodeSizeLimitExceeded            = 4
+  ResultCodeCompareFalse                 = 5
+  ResultCodeCompareTrue                  = 6
+  ResultCodeAuthMethodNotSupported       = 7
+  ResultCodeStrongerAuthRequired         = 8
+  ResultCodeReferral                     = 10
+  ResultCodeAdminLimitExceeded           = 11
+  ResultCodeUnavailableCriticalExtension = 12
+  ResultCodeConfidentialityRequired      = 13
+  ResultCodeSaslBindInProgress           = 14
+  ResultCodeNoSuchAttribute              = 16
+  ResultCodeUndefinedAttributeType       = 17
+  ResultCodeInappropriateMatching        = 18
+  ResultCodeConstraintViolation          = 19
+  ResultCodeAttributeOrValueExists       = 20
+  ResultCodeInvalidAttributeSyntax       = 21
+  ResultCodeNoSuchObject                 = 32
+  ResultCodeAliasProblem                 = 33
+  ResultCodeInvalidDNSyntax              = 34
+  ResultCodeAliasDereferencingProblem    = 36
+  ResultCodeInappropriateAuthentication  = 48
+  ResultCodeInvalidCredentials           = 49
+  ResultCodeInsufficientAccessRights     = 50
+  ResultCodeBusy                         = 51
+  ResultCodeUnavailable                  = 52
+  ResultCodeUnwillingToPerform           = 53
+  ResultCodeNamingViolation              = 64
+  ResultCodeObjectClassViolation         = 65
+  ResultCodeNotAllowedOnNonLeaf          = 66
+  ResultCodeNotAllowedOnRDN              = 67
+  ResultCodeEntryAlreadyExists           = 68
+  ResultCodeObjectClassModsProhibited    = 69
+  ResultCodeAffectsMultipleDSAs          = 71
+  ResultCodeOther                        = 80
+
+  # https://tools.ietf.org/html/rfc4511#appendix-A.1
+  ResultCodesNonError = [
+    ResultCodeSuccess,
+    ResultCodeCompareFalse,
+    ResultCodeCompareTrue,
+    ResultCodeReferral,
+    ResultCodeSaslBindInProgress
+  ]
+
+  # nonstandard list of "successful" result codes for searches
+  ResultCodesSearchSuccess = [
+    ResultCodeSuccess,
+    ResultCodeTimeLimitExceeded,
+    ResultCodeSizeLimitExceeded
+  ]
+
+  # map of result code to human message
   ResultStrings = {
-    0 => "Success",
-    1 => "Operations Error",
-    2 => "Protocol Error",
-    3 => "Time Limit Exceeded",
-    4 => "Size Limit Exceeded",
-    10 => "Referral",
-    12 => "Unavailable crtical extension",
-    14 => "saslBindInProgress",
-    16 => "No Such Attribute",
-    17 => "Undefined Attribute Type",
-    19 => "Constraint Violation",
-    20 => "Attribute or Value Exists",
-    32 => "No Such Object",
-    34 => "Invalid DN Syntax",
-    48 => "Inappropriate Authentication",
-    49 => "Invalid Credentials",
-    50 => "Insufficient Access Rights",
-    51 => "Busy",
-    52 => "Unavailable",
-    53 => "Unwilling to perform",
-    65 => "Object Class Violation",
-    68 => "Entry Already Exists"
+    ResultCodeSuccess                      => "Success",
+    ResultCodeOperationsError              => "Operations Error",
+    ResultCodeProtocolError                => "Protocol Error",
+    ResultCodeTimeLimitExceeded            => "Time Limit Exceeded",
+    ResultCodeSizeLimitExceeded            => "Size Limit Exceeded",
+    ResultCodeCompareFalse                 => "False Comparison",
+    ResultCodeCompareTrue                  => "True Comparison",
+    ResultCodeAuthMethodNotSupported       => "Auth Method Not Supported",
+    ResultCodeStrongerAuthRequired         => "Stronger Auth Needed",
+    ResultCodeReferral                     => "Referral",
+    ResultCodeAdminLimitExceeded           => "Admin Limit Exceeded",
+    ResultCodeUnavailableCriticalExtension => "Unavailable crtical extension",
+    ResultCodeConfidentialityRequired      => "Confidentiality Required",
+    ResultCodeSaslBindInProgress           => "saslBindInProgress",
+    ResultCodeNoSuchAttribute              => "No Such Attribute",
+    ResultCodeUndefinedAttributeType       => "Undefined Attribute Type",
+    ResultCodeInappropriateMatching        => "Inappropriate Matching",
+    ResultCodeConstraintViolation          => "Constraint Violation",
+    ResultCodeAttributeOrValueExists       => "Attribute or Value Exists",
+    ResultCodeInvalidAttributeSyntax       => "Invalide Attribute Syntax",
+    ResultCodeNoSuchObject                 => "No Such Object",
+    ResultCodeAliasProblem                 => "Alias Problem",
+    ResultCodeInvalidDNSyntax              => "Invalid DN Syntax",
+    ResultCodeAliasDereferencingProblem    => "Alias Dereferencing Problem",
+    ResultCodeInappropriateAuthentication  => "Inappropriate Authentication",
+    ResultCodeInvalidCredentials           => "Invalid Credentials",
+    ResultCodeInsufficientAccessRights     => "Insufficient Access Rights",
+    ResultCodeBusy                         => "Busy",
+    ResultCodeUnavailable                  => "Unavailable",
+    ResultCodeUnwillingToPerform           => "Unwilling to perform",
+    ResultCodeNamingViolation              => "Naming Violation",
+    ResultCodeObjectClassViolation         => "Object Class Violation",
+    ResultCodeNotAllowedOnNonLeaf          => "Not Allowed On Non-Leaf",
+    ResultCodeNotAllowedOnRDN              => "Not Allowed On RDN",
+    ResultCodeEntryAlreadyExists           => "Entry Already Exists",
+    ResultCodeObjectClassModsProhibited    => "ObjectClass Modifications Prohibited",
+    ResultCodeAffectsMultipleDSAs          => "Affects Multiple DSAs",
+    ResultCodeOther                        => "Other"
   }
 
   module LDAPControls
@@ -397,7 +471,7 @@ class Net::LDAP
     @verbose = false # Make this configurable with a switch on the class.
     @auth = args[:auth] || DefaultAuth
     @base = args[:base] || DefaultTreebase
-		@force_no_page = args[:force_no_page] || DefaultForceNoPage
+    @force_no_page = args[:force_no_page] || DefaultForceNoPage
     encryption args[:encryption] # may be nil
 
     if pr = @auth[:password] and pr.respond_to?(:call)
@@ -462,10 +536,6 @@ class Net::LDAP
   # additional capabilities are added, more configuration values will be
   # added here.
   #
-  # Currently, the only supported argument is { :method => :simple_tls }.
-  # (Equivalently, you may pass the symbol :simple_tls all by itself,
-  # without enclosing it in a Hash.)
-  #
   # The :simple_tls encryption method encrypts <i>all</i> communications
   # with the LDAP server. It completely establishes SSL/TLS encryption with
   # the LDAP server before any LDAP-protocol data is exchanged. There is no
@@ -485,13 +555,33 @@ class Net::LDAP
   # standard port for simple-TLS encrypted connections is 636. Be sure you
   # are using the correct port.
   #
-  # <i>[Note: a future version of Net::LDAP will support the STARTTLS LDAP
-  # control, which will enable encrypted communications on the same TCP port
-  # used for unencrypted connections.]</i>
+  # The :start_tls like the :simple_tls encryption method also encrypts all
+  # communcations with the LDAP server. With the exception that it operates
+  # over the standard TCP port.
+  #
+  # In order to verify certificates and enable other TLS options, the
+  # :tls_options hash can be passed alongside :simple_tls or :start_tls.
+  # This hash contains any options that can be passed to
+  # OpenSSL::SSL::SSLContext#set_params(). The most common options passed
+  # should be OpenSSL::SSL::SSLContext::DEFAULT_PARAMS, or the :ca_file option,
+  # which contains a path to a Certificate Authority file (PEM-encoded).
+  #
+  # Example for a default setup without custom settings:
+  #   {
+  #     :method => :simple_tls,
+  #     :tls_options => OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
+  #   }
+  #
+  # Example for specifying a CA-File and only allowing TLSv1.1 connections:
+  #
+  #   {
+  #     :method => :start_tls,
+  #     :tls_options => { :ca_file => "/etc/cafile.pem", :ssl_version => "TLSv1_1" }
+  #   }
   def encryption(args)
     case args
     when :simple_tls, :start_tls
-      args = { :method => args }
+      args = { :method => args, :tls_options => {} }
     end
     @encryption = args
   end
@@ -548,7 +638,7 @@ class Net::LDAP
     elsif result
       os.code = result
     else
-      os.code = 0
+      os.code = Net::LDAP::ResultCodeSuccess
     end
     os.message = Net::LDAP.result2string(os.code)
     os
@@ -575,16 +665,11 @@ class Net::LDAP
     # anything with the bind results. We then pass self to the caller's
     # block, where he will execute his LDAP operations. Of course they will
     # all generate auth failures if the bind was unsuccessful.
-    raise Net::LDAP::LdapError, "Open already in progress" if @open_connection
+    raise Net::LDAP::AlreadyOpenedError, "Open already in progress" if @open_connection
 
     instrument "open.net_ldap" do |payload|
       begin
-        @open_connection =
-          Net::LDAP::Connection.new \
-            :host                    => @host,
-            :port                    => @port,
-            :encryption              => @encryption,
-            :instrumentation_service => @instrumentation_service
+        @open_connection = new_connection
         payload[:connection] = @open_connection
         payload[:bind]       = @open_connection.bind(@auth)
         yield self
@@ -609,6 +694,7 @@ class Net::LDAP
   #   Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.)
   # * :size (an integer indicating the maximum number of search entries to
   #   return. Default is zero, which signifies no limit.)
+  # * :time (an integer restricting the maximum time in seconds allowed for a search. Default is zero, no time limit RFC 4511 4.5.1.5)
   # * :deref (one of: Net::LDAP::DerefAliases_Never, Net::LDAP::DerefAliases_Search,
   #   Net::LDAP::DerefAliases_Find, Net::LDAP::DerefAliases_Always. Default is Never.)
   #
@@ -653,31 +739,19 @@ class Net::LDAP
     result_set = return_result_set ? [] : nil
 
     instrument "search.net_ldap", args do |payload|
-      if @open_connection
-        @result = @open_connection.search(args) { |entry|
+      @result = use_connection(args) do |conn|
+        conn.search(args) { |entry|
           result_set << entry if result_set
           yield entry if block_given?
         }
-      else
-        begin
-          conn = Net::LDAP::Connection.new \
-            :host                    => @host,
-            :port                    => @port,
-            :encryption              => @encryption,
-            :instrumentation_service => @instrumentation_service
-          if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
-            @result = conn.search(args) { |entry|
-              result_set << entry if result_set
-              yield entry if block_given?
-            }
-          end
-        ensure
-          conn.close if conn
-        end
       end
 
       if return_result_set
-        (!@result.nil? && @result.result_code == 0) ? result_set : nil
+        unless @result.nil?
+          if ResultCodesSearchSuccess.include?(@result.result_code)
+            result_set
+          end
+        end
       else
         @result.success?
       end
@@ -748,11 +822,7 @@ class Net::LDAP
         payload[:bind]       = @result = @open_connection.bind(auth)
       else
         begin
-          conn = Connection.new \
-            :host                    => @host,
-            :port                    => @port,
-            :encryption              => @encryption,
-            :instrumentation_service => @instrumentation_service
+          conn = new_connection
           payload[:connection] = conn
           payload[:bind]       = @result = conn.bind(auth)
         ensure
@@ -850,22 +920,8 @@ class Net::LDAP
   #  end
   def add(args)
     instrument "add.net_ldap", args do |payload|
-      if @open_connection
-        @result = @open_connection.add(args)
-      else
-        @result = 0
-        begin
-          conn = Connection.new \
-            :host                    => @host,
-            :port                    => @port,
-            :encryption              => @encryption,
-            :instrumentation_service => @instrumentation_service
-          if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
-            @result = conn.add(args)
-          end
-        ensure
-          conn.close if conn
-        end
+      @result = use_connection(args) do |conn|
+        conn.add(args)
       end
       @result.success?
     end
@@ -894,9 +950,10 @@ class Net::LDAP
   # operations in order.
   #
   # Each of the operations appearing in the Array must itself be an Array
-  # with exactly three elements: an operator:: must be :add, :replace, or
-  # :delete an attribute name:: the attribute name (string or symbol) to
-  # modify a value:: either a string or an array of strings.
+  # with exactly three elements:
+  # an operator :: must be :add, :replace, or :delete
+  # an attribute name :: the attribute name (string or symbol) to modify
+  # a value :: either a string or an array of strings.
   #
   # The :add operator will, unsurprisingly, add the specified values to the
   # specified attribute. If the attribute does not already exist, :add will
@@ -939,13 +996,13 @@ class Net::LDAP
   # may not get extended information that will tell you which one failed.
   # #modify has no notion of an atomic transaction. If you specify a chain
   # of modifications in one call to #modify, and one of them fails, the
-  # preceding ones will usually not be "rolled back, " resulting in a
+  # preceding ones will usually not be "rolled back", resulting in a
   # partial update. This is a limitation of the LDAP protocol, not of
   # Net::LDAP.
   #
   # The lack of transactional atomicity in LDAP means that you're usually
   # better off using the convenience methods #add_attribute,
-  # #replace_attribute, and #delete_attribute, which are are wrappers over
+  # #replace_attribute, and #delete_attribute, which are wrappers over
   # #modify. However, certain LDAP servers may provide concurrency
   # semantics, in which the several operations contained in a single #modify
   # call are not interleaved with other modification-requests received
@@ -953,24 +1010,9 @@ class Net::LDAP
   # does _not_ imply transactional atomicity, which LDAP does not provide.
   def modify(args)
     instrument "modify.net_ldap", args do |payload|
-      if @open_connection
-        @result = @open_connection.modify(args)
-      else
-        @result = 0
-        begin
-          conn = Connection.new \
-            :host                    => @host,
-            :port                    => @port,
-            :encryption              => @encryption,
-            :instrumentation_service => @instrumentation_service
-          if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
-            @result = conn.modify(args)
-          end
-        ensure
-          conn.close if conn
-        end
+      @result = use_connection(args) do |conn|
+        conn.modify(args)
       end
-
       @result.success?
     end
   end
@@ -1030,22 +1072,8 @@ class Net::LDAP
   # _Documentation_ _stub_
   def rename(args)
     instrument "rename.net_ldap", args do |payload|
-      if @open_connection
-        @result = @open_connection.rename(args)
-      else
-        @result = 0
-        begin
-          conn = Connection.new \
-            :host                    => @host,
-            :port                    => @port,
-            :encryption              => @encryption,
-            :instrumentation_service => @instrumentation_service
-          if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
-            @result = conn.rename(args)
-          end
-        ensure
-          conn.close if conn
-        end
+      @result = use_connection(args) do |conn|
+        conn.rename(args)
       end
       @result.success?
     end
@@ -1063,22 +1091,8 @@ class Net::LDAP
   #  ldap.delete :dn => dn
   def delete(args)
     instrument "delete.net_ldap", args do |payload|
-      if @open_connection
-        @result = @open_connection.delete(args)
-      else
-        @result = 0
-        begin
-          conn = Connection.new \
-            :host                    => @host,
-            :port                    => @port,
-            :encryption              => @encryption,
-            :instrumentation_service => @instrumentation_service
-          if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
-            @result = conn.delete(args)
-          end
-        ensure
-          conn.close
-        end
+      @result = use_connection(args) do |conn|
+        conn.delete(args)
       end
       @result.success?
     end
@@ -1113,9 +1127,16 @@ class Net::LDAP
   def search_root_dse
     rs = search(:ignore_server_caps => true, :base => "",
                 :scope => SearchScope_BaseObject,
-                :attributes => [ :namingContexts, :supportedLdapVersion,
-                  :altServer, :supportedControl, :supportedExtension,
-                  :supportedFeatures, :supportedSASLMechanisms])
+                :attributes => [
+                  :altServer,
+                  :namingContexts,
+                  :supportedCapabilities,
+                  :supportedControl,
+                  :supportedExtension,
+                  :supportedFeatures,
+                  :supportedLdapVersion,
+                  :supportedSASLMechanisms
+                ])
     (rs and rs.first) or Net::LDAP::Entry.new
   end
 
@@ -1166,585 +1187,43 @@ class Net::LDAP
   # MUST refactor the root_dse call out.
   #++
   def paged_searches_supported?
-		# active directory returns that it supports paged results. However
-		# it returns binary data in the rfc2696_cookie which throws an
-		# encoding exception breaking searching.
-		return false if @force_no_page
+    # active directory returns that it supports paged results. However
+    # it returns binary data in the rfc2696_cookie which throws an
+    # encoding exception breaking searching.
+    return false if @force_no_page
     @server_caps ||= search_root_dse
     @server_caps[:supportedcontrol].include?(Net::LDAP::LDAPControls::PAGED_RESULTS)
   end
-end # class LDAP
-
-# This is a private class used internally by the library. It should not
-# be called by user code.
-class Net::LDAP::Connection #:nodoc:
-  include Net::LDAP::Instrumentation
-
-  LdapVersion = 3
-  MaxSaslChallenges = 10
-
-  def initialize(server)
-    @instrumentation_service = server[:instrumentation_service]
-
-    begin
-      @conn = TCPSocket.new(server[:host], server[:port])
-    rescue SocketError
-      raise Net::LDAP::LdapError, "No such address or other socket error."
-    rescue Errno::ECONNREFUSED
-      raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}."
-    end
-
-    if server[:encryption]
-      setup_encryption server[:encryption]
-    end
-
-    yield self if block_given?
-  end
-
-  module GetbyteForSSLSocket
-    def getbyte
-      getc.ord
-    end
-  end
-
-  module FixSSLSocketSyncClose
-    def close
-      super
-      io.close
-    end
-  end
-
-  def self.wrap_with_ssl(io)
-    raise Net::LDAP::LdapError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL
-    ctx = OpenSSL::SSL::SSLContext.new
-    conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
-    conn.connect
-
-    # Doesn't work:
-    # conn.sync_close = true
 
-    conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
-    conn.extend(FixSSLSocketSyncClose)
+  private
 
-    conn
-  end
-
-  #--
-  # Helper method called only from new, and only after we have a
-  # successfully-opened @conn instance variable, which is a TCP connection.
-  # Depending on the received arguments, we establish SSL, potentially
-  # replacing the value of @conn accordingly. Don't generate any errors here
-  # if no encryption is requested. DO raise Net::LDAP::LdapError objects if encryption
-  # is requested and we have trouble setting it up. That includes if OpenSSL
-  # is not set up on the machine. (Question: how does the Ruby OpenSSL
-  # wrapper react in that case?) DO NOT filter exceptions raised by the
-  # OpenSSL library. Let them pass back to the user. That should make it
-  # easier for us to debug the problem reports. Presumably (hopefully?) that
-  # will also produce recognizable errors if someone tries to use this on a
-  # machine without OpenSSL.
-  #
-  # The simple_tls method is intended as the simplest, stupidest, easiest
-  # solution for people who want nothing more than encrypted comms with the
-  # LDAP server. It doesn't do any server-cert validation and requires
-  # nothing in the way of key files and root-cert files, etc etc. OBSERVE:
-  # WE REPLACE the value of @conn, which is presumed to be a connected
-  # TCPSocket object.
-  #
-  # The start_tls method is supported by many servers over the standard LDAP
-  # port. It does not require an alternative port for encrypted
-  # communications, as with simple_tls. Thanks for Kouhei Sutou for
-  # generously contributing the :start_tls path.
-  #++
-  def setup_encryption(args)
-    case args[:method]
-    when :simple_tls
-      @conn = self.class.wrap_with_ssl(@conn)
-      # additional branches requiring server validation and peer certs, etc.
-      # go here.
-    when :start_tls
-      msgid = next_msgid.to_ber
-      request = [Net::LDAP::StartTlsOid.to_ber_contextspecific(0)].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
-      request_pkt = [msgid, request].to_ber_sequence
-      write request_pkt
-      be = read
-      raise Net::LDAP::LdapError, "no start_tls result" if be.nil?
-      pdu = Net::LDAP::PDU.new(be)
-      raise Net::LDAP::LdapError, "no start_tls result" if pdu.nil?
-      if pdu.result_code.zero?
-        @conn = self.class.wrap_with_ssl(@conn)
-      else
-        raise Net::LDAP::LdapError, "start_tls failed: #{pdu.result_code}"
-      end
+  # Yields an open connection if there is one, otherwise establishes a new
+  # connection, binds, and yields it. If binding fails, it will return the
+  # result from that, and :use_connection: will not yield at all. If not
+  # the return value is whatever is returned from the block.
+  def use_connection(args)
+    if @open_connection
+      yield @open_connection
     else
-      raise Net::LDAP::LdapError, "unsupported encryption method #{args[:method]}"
-    end
-  end
-
-  #--
-  # This is provided as a convenience method to make sure a connection
-  # object gets closed without waiting for a GC to happen. Clients shouldn't
-  # have to call it, but perhaps it will come in handy someday.
-  #++
-  def close
-    @conn.close
-    @conn = nil
-  end
-
-  # Internal: Reads and parses data from the configured connection.
-  #
-  # - syntax: the BER syntax to use to parse the read data with
-  #
-  # Returns basic BER objects.
-  def read(syntax = Net::LDAP::AsnSyntax)
-    instrument "read.net_ldap_connection", :syntax => syntax do |payload|
-      @conn.read_ber(syntax) do |id, content_length|
-        payload[:object_type_id] = id
-        payload[:content_length] = content_length
-      end
-    end
-  end
-  private :read
-
-  # Internal: Writes the given packet to the configured connection.
-  #
-  # - packet: the BER data packet to write on the socket.
-  #
-  # Returns the return value from writing to the connection, which in some
-  # cases is the Integer number of bytes written to the socket.
-  def write(packet)
-    instrument "write.net_ldap_connection" do |payload|
-      payload[:content_length] = @conn.write(packet)
-    end
-  end
-  private :write
-
-  def next_msgid
-    @msgid ||= 0
-    @msgid += 1
-  end
-
-  def bind(auth)
-    instrument "bind.net_ldap_connection" do |payload|
-      payload[:method] = meth = auth[:method]
-      if [:simple, :anonymous, :anon].include?(meth)
-        bind_simple auth
-      elsif meth == :sasl
-        bind_sasl(auth)
-      elsif meth == :gss_spnego
-        bind_gss_spnego(auth)
-      else
-        raise Net::LDAP::LdapError, "Unsupported auth method (#{meth})"
-      end
-    end
-  end
-
-  #--
-  # Implements a simple user/psw authentication. Accessed by calling #bind
-  # with a method of :simple or :anonymous.
-  #++
-  def bind_simple(auth)
-    user, psw = if auth[:method] == :simple
-                  [auth[:username] || auth[:dn], auth[:password]]
-                else
-                  ["", ""]
-                end
-
-    raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
-
-    msgid = next_msgid.to_ber
-    request = [LdapVersion.to_ber, user.to_ber,
-      psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
-    request_pkt = [msgid, request].to_ber_sequence
-    write request_pkt
-
-    (be = read and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
-
-    pdu
-  end
-
-  #--
-  # Required parameters: :mechanism, :initial_credential and
-  # :challenge_response
-  #
-  # Mechanism is a string value that will be passed in the SASL-packet's
-  # "mechanism" field.
-  #
-  # Initial credential is most likely a string. It's passed in the initial
-  # BindRequest that goes to the server. In some protocols, it may be empty.
-  #
-  # Challenge-response is a Ruby proc that takes a single parameter and
-  # returns an object that will typically be a string. The
-  # challenge-response block is called when the server returns a
-  # BindResponse with a result code of 14 (saslBindInProgress). The
-  # challenge-response block receives a parameter containing the data
-  # returned by the server in the saslServerCreds field of the LDAP
-  # BindResponse packet. The challenge-response block may be called multiple
-  # times during the course of a SASL authentication, and each time it must
-  # return a value that will be passed back to the server as the credential
-  # data in the next BindRequest packet.
-  #++
-  def bind_sasl(auth)
-    mech, cred, chall = auth[:mechanism], auth[:initial_credential],
-      auth[:challenge_response]
-    raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && cred && chall)
-
-    n = 0
-    loop {
-      msgid = next_msgid.to_ber
-      sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
-      request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0)
-      request_pkt = [msgid, request].to_ber_sequence
-      write request_pkt
-
-      (be = read and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
-      return pdu unless pdu.result_code == 14 # saslBindInProgress
-      raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
-
-      cred = chall.call(pdu.result_server_sasl_creds)
-    }
-
-    raise Net::LDAP::LdapError, "why are we here?"
-  end
-  private :bind_sasl
-
-  #--
-  # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
-  # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
-  # integrate it without introducing an external dependency.
-  #
-  # This authentication method is accessed by calling #bind with a :method
-  # parameter of :gss_spnego. It requires :username and :password
-  # attributes, just like the :simple authentication method. It performs a
-  # GSS-SPNEGO authentication with the server, which is presumed to be a
-  # Microsoft Active Directory.
-  #++
-  def bind_gss_spnego(auth)
-    require 'ntlm'
-
-    user, psw = [auth[:username] || auth[:dn], auth[:password]]
-    raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
-
-    nego = proc { |challenge|
-      t2_msg = NTLM::Message.parse(challenge)
-      t3_msg = t2_msg.response({ :user => user, :password => psw },
-                               { :ntlmv2 => true })
-      t3_msg.serialize
-    }
-
-    bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO",
-              :initial_credential => NTLM::Message::Type1.new.serialize,
-              :challenge_response => nego)
-  end
-  private :bind_gss_spnego
-
-
-  #--
-  # Allow the caller to specify a sort control
-  #
-  # The format of the sort control needs to be:
-  #
-  # :sort_control => ["cn"]  # just a string
-  # or
-  # :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false)
-  # or
-  # :sort_control => ["givenname","sn"] #multiple strings or arrays
-  #
-  def encode_sort_controls(sort_definitions)
-    return sort_definitions unless sort_definitions
-
-    sort_control_values = sort_definitions.map do |control|
-      control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder
-      control[0] = String(control[0]).to_ber,
-      control[1] = String(control[1]).to_ber,
-      control[2] = (control[2] == true).to_ber
-      control.to_ber_sequence
-    end
-    sort_control = [
-      Net::LDAP::LDAPControls::SORT_REQUEST.to_ber,
-      false.to_ber,
-      sort_control_values.to_ber_sequence.to_s.to_ber
-    ].to_ber_sequence
-  end
-
-  #--
-  # Alternate implementation, this yields each search entry to the caller as
-  # it are received.
-  #
-  # TODO: certain search parameters are hardcoded.
-  # TODO: if we mis-parse the server results or the results are wrong, we
-  # can block forever. That's because we keep reading results until we get a
-  # type-5 packet, which might never come. We need to support the time-limit
-  # in the protocol.
-  #++
-  def search(args = {})
-    search_filter = (args && args[:filter]) ||
-      Net::LDAP::Filter.eq("objectclass", "*")
-    search_filter = Net::LDAP::Filter.construct(search_filter) if search_filter.is_a?(String)
-    search_base = (args && args[:base]) || "dc=example, dc=com"
-    search_attributes = ((args && args[:attributes]) || []).map { |attr| attr.to_s.to_ber}
-    return_referrals = args && args[:return_referrals] == true
-    sizelimit = (args && args[:size].to_i) || 0
-    raise Net::LDAP::LdapError, "invalid search-size" unless sizelimit >= 0
-    paged_searches_supported = (args && args[:paged_searches_supported])
-
-    attributes_only = (args and args[:attributes_only] == true)
-    scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
-    raise Net::LDAP::LdapError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
-
-    sort_control = encode_sort_controls(args.fetch(:sort_controls){ false })
-
-	deref = args[:deref] || Net::LDAP::DerefAliases_Never
-	raise Net::LDAP::LdapError.new( "invalid alias dereferencing value" ) unless Net::LDAP::DerefAliasesArray.include?(deref)
-
-
-    # An interesting value for the size limit would be close to A/D's
-    # built-in page limit of 1000 records, but openLDAP newer than version
-    # 2.2.0 chokes on anything bigger than 126. You get a silent error that
-    # is easily visible by running slapd in debug mode. Go figure.
-    #
-    # Changed this around 06Sep06 to support a caller-specified search-size
-    # limit. Because we ALWAYS do paged searches, we have to work around the
-    # problem that it's not legal to specify a "normal" sizelimit (in the
-    # body of the search request) that is larger than the page size we're
-    # requesting. Unfortunately, I have the feeling that this will break
-    # with LDAP servers that don't support paged searches!!!
-    #
-    # (Because we pass zero as the sizelimit on search rounds when the
-    # remaining limit is larger than our max page size of 126. In these
-    # cases, I think the caller's search limit will be ignored!)
-    #
-    # CONFIRMED: This code doesn't work on LDAPs that don't support paged
-    # searches when the size limit is larger than 126. We're going to have
-    # to do a root-DSE record search and not do a paged search if the LDAP
-    # doesn't support it. Yuck.
-    rfc2696_cookie = [126, ""]
-    result_pdu = nil
-    n_results = 0
-
-    instrument "search.net_ldap_connection",
-               :filter     => search_filter,
-               :base       => search_base,
-               :scope      => scope,
-               :limit      => sizelimit,
-               :sort       => sort_control,
-               :referrals  => return_referrals,
-               :deref      => deref,
-               :attributes => search_attributes do |payload|
-      loop do
-        # should collect this into a private helper to clarify the structure
-        query_limit = 0
-        if sizelimit > 0
-          if paged_searches_supported
-            query_limit = (((sizelimit - n_results) < 126) ? (sizelimit -
-                                                              n_results) : 0)
-          else
-            query_limit = sizelimit
-          end
-        end
-
-        request = [
-          search_base.to_ber,
-          scope.to_ber_enumerated,
-          deref.to_ber_enumerated,
-          query_limit.to_ber, # size limit
-          0.to_ber,
-          attributes_only.to_ber,
-          search_filter.to_ber,
-          search_attributes.to_ber_sequence
-        ].to_ber_appsequence(3)
-
-  			# rfc2696_cookie sometimes contains binary data from Microsoft Active Directory
-  			# this breaks when calling to_ber. (Can't force binary data to UTF-8)
-  			# we have to disable paging (even though server supports it) to get around this...
-
-        controls = []
-        controls <<
-          [
-            Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,
-            # Criticality MUST be false to interoperate with normal LDAPs.
-            false.to_ber,
-            rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber
-          ].to_ber_sequence if paged_searches_supported
-        controls << sort_control if sort_control
-        controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
-
-        pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence
-        write pkt
-
-        result_pdu = nil
-        controls = []
-
-        while (be = read) && (pdu = Net::LDAP::PDU.new(be))
-          case pdu.app_tag
-          when Net::LDAP::PDU::SearchReturnedData
-            n_results += 1
-            yield pdu.search_entry if block_given?
-          when Net::LDAP::PDU::SearchResultReferral
-            if return_referrals
-              if block_given?
-                se = Net::LDAP::Entry.new
-                se[:search_referrals] = (pdu.search_referrals || [])
-                yield se
-              end
-            end
-          when Net::LDAP::PDU::SearchResult
-            result_pdu = pdu
-            controls = pdu.result_controls
-            if return_referrals && pdu.result_code == 10
-              if block_given?
-                se = Net::LDAP::Entry.new
-                se[:search_referrals] = (pdu.search_referrals || [])
-                yield se
-              end
-            end
-            break
-          else
-            raise Net::LDAP::LdapError, "invalid response-type in search: #{pdu.app_tag}"
-          end
-        end
-
-        # count number of pages of results
-        payload[:page_count] ||= 0
-        payload[:page_count]  += 1
-
-        # When we get here, we have seen a type-5 response. If there is no
-        # error AND there is an RFC-2696 cookie, then query again for the next
-        # page of results. If not, we're done. Don't screw this up or we'll
-        # break every search we do.
-        #
-        # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't
-        # that have a parameter of AsnSyntax? Does this just accidentally
-        # work? According to RFC-2696, the value expected in this position is
-        # of type OCTET STRING, covered in the default syntax supported by
-        # read_ber, so I guess we're ok.
-        more_pages = false
-        if result_pdu.result_code == 0 and controls
-          controls.each do |c|
-            if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS
-              # just in case some bogus server sends us more than 1 of these.
-              more_pages = false
-              if c.value and c.value.length > 0
-                cookie = c.value.read_ber[1]
-                if cookie and cookie.length > 0
-                  rfc2696_cookie[1] = cookie
-                  more_pages = true
-                end
-              end
-            end
-          end
+      begin
+        conn = new_connection
+        if (result = conn.bind(args[:auth] || @auth)).result_code == Net::LDAP::ResultCodeSuccess
+          yield conn
+        else
+          return result
         end
-
-        break unless more_pages
-      end # loop
-
-      # track total result count
-      payload[:result_count] = n_results
-
-      result_pdu || OpenStruct.new(:status => :failure, :result_code => 1, :message => "Invalid search")
-    end # instrument
-  end
-
-  MODIFY_OPERATIONS = { #:nodoc:
-    :add => 0,
-    :delete => 1,
-    :replace => 2
-  }
-
-  def self.modify_ops(operations)
-    ops = []
-    if operations
-      operations.each { |op, attrib, values|
-        # TODO, fix the following line, which gives a bogus error if the
-        # opcode is invalid.
-        op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated
-        values = [ values ].flatten.map { |v| v.to_ber if v }.to_ber_set
-        values = [ attrib.to_s.to_ber, values ].to_ber_sequence
-        ops << [ op_ber, values ].to_ber
-      }
+      ensure
+        conn.close if conn
+      end
     end
-    ops
-  end
-
-  #--
-  # TODO: need to support a time limit, in case the server fails to respond.
-  # TODO: We're throwing an exception here on empty DN. Should return a
-  # proper error instead, probaby from farther up the chain.
-  # TODO: If the user specifies a bogus opcode, we'll throw a confusing
-  # error here ("to_ber_enumerated is not defined on nil").
-  #++
-  def modify(args)
-    modify_dn = args[:dn] or raise "Unable to modify empty DN"
-    ops = self.class.modify_ops args[:operations]
-    request = [ modify_dn.to_ber,
-      ops.to_ber_sequence ].to_ber_appsequence(6)
-    pkt = [ next_msgid.to_ber, request ].to_ber_sequence
-    write pkt
-
-    (be = read) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == Net::LDAP::PDU::ModifyResponse) or raise Net::LDAP::LdapError, "response missing or invalid"
-
-    pdu
-  end
-
-  #--
-  # TODO: need to support a time limit, in case the server fails to respond.
-  # Unlike other operation-methods in this class, we return a result hash
-  # rather than a simple result number. This is experimental, and eventually
-  # we'll want to do this with all the others. The point is to have access
-  # to the error message and the matched-DN returned by the server.
-  #++
-  def add(args)
-    add_dn = args[:dn] or raise Net::LDAP::LdapError, "Unable to add empty DN"
-    add_attrs = []
-    a = args[:attributes] and a.each { |k, v|
-      add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence
-    }
-
-    request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
-    pkt = [next_msgid.to_ber, request].to_ber_sequence
-    write pkt
-
-    (be = read) &&
-      (pdu = Net::LDAP::PDU.new(be)) &&
-      (pdu.app_tag == Net::LDAP::PDU::AddResponse) or
-      raise Net::LDAP::LdapError, "response missing or invalid"
-
-    pdu
   end
 
-  #--
-  # TODO: need to support a time limit, in case the server fails to respond.
-  #++
-  def rename(args)
-    old_dn = args[:olddn] or raise "Unable to rename empty DN"
-    new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
-    delete_attrs = args[:delete_attributes] ? true : false
-    new_superior = args[:new_superior]
-
-    request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber]
-    request << new_superior.to_ber_contextspecific(0) unless new_superior == nil
-
-    pkt = [next_msgid.to_ber, request.to_ber_appsequence(12)].to_ber_sequence
-    write pkt
-
-    (be = read) &&
-    (pdu = Net::LDAP::PDU.new( be )) && (pdu.app_tag == Net::LDAP::PDU::ModifyRDNResponse) or
-    raise Net::LDAP::LdapError.new( "response missing or invalid" )
-
-    pdu
+  # Establish a new connection to the LDAP server
+  def new_connection
+    Net::LDAP::Connection.new \
+      :host                    => @host,
+      :port                    => @port,
+      :encryption              => @encryption,
+      :instrumentation_service => @instrumentation_service
   end
-
-  #--
-  # TODO, need to support a time limit, in case the server fails to respond.
-  #++
-  def delete(args)
-    dn = args[:dn] or raise "Unable to delete empty DN"
-    controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later
-    request = dn.to_s.to_ber_application_string(10)
-    pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence
-    write pkt
-
-    (be = read) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == Net::LDAP::PDU::DeleteResponse) or raise Net::LDAP::LdapError, "response missing or invalid"
-
-    pdu
-  end
-end # class Connection
+end # class LDAP
diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb
new file mode 100644
index 0000000..5e735f5
--- /dev/null
+++ b/lib/net/ldap/connection.rb
@@ -0,0 +1,702 @@
+# This is a private class used internally by the library. It should not
+# be called by user code.
+class Net::LDAP::Connection #:nodoc:
+  include Net::LDAP::Instrumentation
+
+  LdapVersion = 3
+  MaxSaslChallenges = 10
+
+  def initialize(server)
+    @instrumentation_service = server[:instrumentation_service]
+
+    begin
+      @conn = server[:socket] || TCPSocket.new(server[:host], server[:port])
+    rescue SocketError
+      raise Net::LDAP::Error, "No such address or other socket error."
+    rescue Errno::ECONNREFUSED
+      raise Net::LDAP::Error, "Server #{server[:host]} refused connection on port #{server[:port]}."
+    rescue Errno::EHOSTUNREACH => error
+      raise Net::LDAP::Error, "Host #{server[:host]} was unreachable (#{error.message})"
+    rescue Errno::ETIMEDOUT
+      raise Net::LDAP::Error, "Connection to #{server[:host]} timed out."
+    end
+
+    if server[:encryption]
+      setup_encryption server[:encryption]
+    end
+
+    yield self if block_given?
+  end
+
+  module GetbyteForSSLSocket
+    def getbyte
+      getc.ord
+    end
+  end
+
+  module FixSSLSocketSyncClose
+    def close
+      super
+      io.close
+    end
+  end
+
+  def self.wrap_with_ssl(io, tls_options = {})
+    raise Net::LDAP::NoOpenSSLError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL
+
+    ctx = OpenSSL::SSL::SSLContext.new
+
+    # By default, we do not verify certificates. For a 1.0 release, this should probably be changed at some point.
+    # See discussion in https://github.com/ruby-ldap/ruby-net-ldap/pull/161
+    ctx.set_params(tls_options) unless tls_options.empty?
+
+    conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
+    conn.connect
+
+    # Doesn't work:
+    # conn.sync_close = true
+
+    conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
+    conn.extend(FixSSLSocketSyncClose)
+
+    conn
+  end
+
+  #--
+  # Helper method called only from new, and only after we have a
+  # successfully-opened @conn instance variable, which is a TCP connection.
+  # Depending on the received arguments, we establish SSL, potentially
+  # replacing the value of @conn accordingly. Don't generate any errors here
+  # if no encryption is requested. DO raise Net::LDAP::Error objects if encryption
+  # is requested and we have trouble setting it up. That includes if OpenSSL
+  # is not set up on the machine. (Question: how does the Ruby OpenSSL
+  # wrapper react in that case?) DO NOT filter exceptions raised by the
+  # OpenSSL library. Let them pass back to the user. That should make it
+  # easier for us to debug the problem reports. Presumably (hopefully?) that
+  # will also produce recognizable errors if someone tries to use this on a
+  # machine without OpenSSL.
+  #
+  # The simple_tls method is intended as the simplest, stupidest, easiest
+  # solution for people who want nothing more than encrypted comms with the
+  # LDAP server. It doesn't do any server-cert validation and requires
+  # nothing in the way of key files and root-cert files, etc etc. OBSERVE:
+  # WE REPLACE the value of @conn, which is presumed to be a connected
+  # TCPSocket object.
+  #
+  # The start_tls method is supported by many servers over the standard LDAP
+  # port. It does not require an alternative port for encrypted
+  # communications, as with simple_tls. Thanks for Kouhei Sutou for
+  # generously contributing the :start_tls path.
+  #++
+  def setup_encryption(args)
+    args[:tls_options] ||= {}
+    case args[:method]
+    when :simple_tls
+      @conn = self.class.wrap_with_ssl(@conn, args[:tls_options])
+      # additional branches requiring server validation and peer certs, etc.
+      # go here.
+    when :start_tls
+      message_id = next_msgid
+      request    = [
+        Net::LDAP::StartTlsOid.to_ber_contextspecific(0)
+      ].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
+
+      write(request, nil, message_id)
+      pdu = queued_read(message_id)
+
+      if pdu.nil? || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
+        raise Net::LDAP::NoStartTLSResultError, "no start_tls result"
+      end
+
+      if pdu.result_code.zero?
+        @conn = self.class.wrap_with_ssl(@conn, args[:tls_options])
+      else
+        raise Net::LDAP::StartTlSError, "start_tls failed: #{pdu.result_code}"
+      end
+    else
+      raise Net::LDAP::EncMethodUnsupportedError, "unsupported encryption method #{args[:method]}"
+    end
+  end
+
+  #--
+  # This is provided as a convenience method to make sure a connection
+  # object gets closed without waiting for a GC to happen. Clients shouldn't
+  # have to call it, but perhaps it will come in handy someday.
+  #++
+  def close
+    @conn.close
+    @conn = nil
+  end
+
+  # Internal: Reads messages by ID from a queue, falling back to reading from
+  # the connected socket until a message matching the ID is read. Any messages
+  # with mismatched IDs gets queued for subsequent reads by the origin of that
+  # message ID.
+  #
+  # Returns a Net::LDAP::PDU object or nil.
+  def queued_read(message_id)
+    if pdu = message_queue[message_id].shift
+      return pdu
+    end
+
+    # read messages until we have a match for the given message_id
+    while pdu = read
+      if pdu.message_id == message_id
+        return pdu
+      else
+        message_queue[pdu.message_id].push pdu
+        next
+      end
+    end
+
+    pdu
+  end
+
+  # Internal: The internal queue of messages, read from the socket, grouped by
+  # message ID.
+  #
+  # Used by `queued_read` to return messages sent by the server with the given
+  # ID. If no messages are queued for that ID, `queued_read` will `read` from
+  # the socket and queue messages that don't match the given ID for other
+  # readers.
+  #
+  # Returns the message queue Hash.
+  def message_queue
+    @message_queue ||= Hash.new do |hash, key|
+      hash[key] = []
+    end
+  end
+
+  # Internal: Reads and parses data from the configured connection.
+  #
+  # - syntax: the BER syntax to use to parse the read data with
+  #
+  # Returns parsed Net::LDAP::PDU object.
+  def read(syntax = Net::LDAP::AsnSyntax)
+    ber_object =
+      instrument "read.net_ldap_connection", :syntax => syntax do |payload|
+        @conn.read_ber(syntax) do |id, content_length|
+          payload[:object_type_id] = id
+          payload[:content_length] = content_length
+        end
+      end
+
+    return unless ber_object
+
+    instrument "parse_pdu.net_ldap_connection" do |payload|
+      pdu = payload[:pdu]  = Net::LDAP::PDU.new(ber_object)
+
+      payload[:message_id] = pdu.message_id
+      payload[:app_tag]    = pdu.app_tag
+
+      pdu
+    end
+  end
+  private :read
+
+  # Internal: Write a BER formatted packet with the next message id to the
+  # configured connection.
+  #
+  # - request: required BER formatted request
+  # - controls: optional BER formatted controls
+  #
+  # Returns the return value from writing to the connection, which in some
+  # cases is the Integer number of bytes written to the socket.
+  def write(request, controls = nil, message_id = next_msgid)
+    instrument "write.net_ldap_connection" do |payload|
+      packet = [message_id.to_ber, request, controls].compact.to_ber_sequence
+      payload[:content_length] = @conn.write(packet)
+    end
+  end
+  private :write
+
+  def next_msgid
+    @msgid ||= 0
+    @msgid += 1
+  end
+
+  def bind(auth)
+    instrument "bind.net_ldap_connection" do |payload|
+      payload[:method] = meth = auth[:method]
+      if [:simple, :anonymous, :anon].include?(meth)
+        bind_simple auth
+      elsif meth == :sasl
+        bind_sasl(auth)
+      elsif meth == :gss_spnego
+        bind_gss_spnego(auth)
+      else
+        raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{meth})"
+      end
+    end
+  end
+
+  #--
+  # Implements a simple user/psw authentication. Accessed by calling #bind
+  # with a method of :simple or :anonymous.
+  #++
+  def bind_simple(auth)
+    user, psw = if auth[:method] == :simple
+                  [auth[:username] || auth[:dn], auth[:password]]
+                else
+                  ["", ""]
+                end
+
+    raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
+
+    message_id = next_msgid
+    request    = [
+      LdapVersion.to_ber, user.to_ber,
+      psw.to_ber_contextspecific(0)
+    ].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
+
+    write(request, nil, message_id)
+    pdu = queued_read(message_id)
+
+    if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
+      raise Net::LDAP::NoBindResultError, "no bind result"
+    end
+
+    pdu
+  end
+
+  #--
+  # Required parameters: :mechanism, :initial_credential and
+  # :challenge_response
+  #
+  # Mechanism is a string value that will be passed in the SASL-packet's
+  # "mechanism" field.
+  #
+  # Initial credential is most likely a string. It's passed in the initial
+  # BindRequest that goes to the server. In some protocols, it may be empty.
+  #
+  # Challenge-response is a Ruby proc that takes a single parameter and
+  # returns an object that will typically be a string. The
+  # challenge-response block is called when the server returns a
+  # BindResponse with a result code of 14 (saslBindInProgress). The
+  # challenge-response block receives a parameter containing the data
+  # returned by the server in the saslServerCreds field of the LDAP
+  # BindResponse packet. The challenge-response block may be called multiple
+  # times during the course of a SASL authentication, and each time it must
+  # return a value that will be passed back to the server as the credential
+  # data in the next BindRequest packet.
+  #++
+  def bind_sasl(auth)
+    mech, cred, chall = auth[:mechanism], auth[:initial_credential],
+      auth[:challenge_response]
+    raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall)
+
+    message_id = next_msgid
+
+    n = 0
+    loop {
+      sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
+      request = [
+        LdapVersion.to_ber, "".to_ber, sasl
+      ].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
+
+      write(request, nil, message_id)
+      pdu = queued_read(message_id)
+
+      if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
+        raise Net::LDAP::NoBindResultError, "no bind result"
+      end
+
+      return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress
+      raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
+
+      cred = chall.call(pdu.result_server_sasl_creds)
+    }
+
+    raise Net::LDAP::SASLChallengeOverflowError, "why are we here?"
+  end
+  private :bind_sasl
+
+  #--
+  # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
+  # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
+  # integrate it without introducing an external dependency.
+  #
+  # This authentication method is accessed by calling #bind with a :method
+  # parameter of :gss_spnego. It requires :username and :password
+  # attributes, just like the :simple authentication method. It performs a
+  # GSS-SPNEGO authentication with the server, which is presumed to be a
+  # Microsoft Active Directory.
+  #++
+  def bind_gss_spnego(auth)
+    require 'ntlm'
+
+    user, psw = [auth[:username] || auth[:dn], auth[:password]]
+    raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
+
+    nego = proc { |challenge|
+      t2_msg = NTLM::Message.parse(challenge)
+      t3_msg = t2_msg.response({ :user => user, :password => psw },
+                               { :ntlmv2 => true })
+      t3_msg.serialize
+    }
+
+    bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO",
+              :initial_credential => NTLM::Message::Type1.new.serialize,
+              :challenge_response => nego)
+  end
+  private :bind_gss_spnego
+
+
+  #--
+  # Allow the caller to specify a sort control
+  #
+  # The format of the sort control needs to be:
+  #
+  # :sort_control => ["cn"]  # just a string
+  # or
+  # :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false)
+  # or
+  # :sort_control => ["givenname","sn"] #multiple strings or arrays
+  #
+  def encode_sort_controls(sort_definitions)
+    return sort_definitions unless sort_definitions
+
+    sort_control_values = sort_definitions.map do |control|
+      control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder
+      control[0] = String(control[0]).to_ber,
+      control[1] = String(control[1]).to_ber,
+      control[2] = (control[2] == true).to_ber
+      control.to_ber_sequence
+    end
+    sort_control = [
+      Net::LDAP::LDAPControls::SORT_REQUEST.to_ber,
+      false.to_ber,
+      sort_control_values.to_ber_sequence.to_s.to_ber
+    ].to_ber_sequence
+  end
+
+  #--
+  # Alternate implementation, this yields each search entry to the caller as
+  # it are received.
+  #
+  # TODO: certain search parameters are hardcoded.
+  # TODO: if we mis-parse the server results or the results are wrong, we
+  # can block forever. That's because we keep reading results until we get a
+  # type-5 packet, which might never come. We need to support the time-limit
+  # in the protocol.
+  #++
+  def search(args = nil)
+    args ||= {}
+
+    # filtering, scoping, search base
+    # filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7
+    # base:   https://tools.ietf.org/html/rfc4511#section-4.5.1.1
+    # scope:  https://tools.ietf.org/html/rfc4511#section-4.5.1.2
+    filter = args[:filter] || Net::LDAP::Filter.eq("objectClass", "*")
+    base   = args[:base]
+    scope  = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
+
+    # attr handling
+    # attrs:      https://tools.ietf.org/html/rfc4511#section-4.5.1.8
+    # attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6
+    attrs  = Array(args[:attributes])
+    attrs_only = args[:attributes_only] == true
+
+    # references
+    # refs:  https://tools.ietf.org/html/rfc4511#section-4.5.3
+    # deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3
+    refs   = args[:return_referrals] == true
+    deref  = args[:deref] || Net::LDAP::DerefAliases_Never
+
+    # limiting, paging, sorting
+    # size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4
+    # time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5
+    size   = args[:size].to_i
+    time   = args[:time].to_i
+    paged  = args[:paged_searches_supported]
+    sort   = args.fetch(:sort_controls, false)
+
+    # arg validation
+    raise ArgumentError, "search base is required" unless base
+    raise ArgumentError, "invalid search-size" unless size >= 0
+    raise ArgumentError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
+    raise ArgumentError, "invalid alias dereferencing value" unless Net::LDAP::DerefAliasesArray.include?(deref)
+
+    # arg transforms
+    filter = Net::LDAP::Filter.construct(filter) if filter.is_a?(String)
+    ber_attrs = attrs.map { |attr| attr.to_s.to_ber }
+    ber_sort  = encode_sort_controls(sort)
+
+    # An interesting value for the size limit would be close to A/D's
+    # built-in page limit of 1000 records, but openLDAP newer than version
+    # 2.2.0 chokes on anything bigger than 126. You get a silent error that
+    # is easily visible by running slapd in debug mode. Go figure.
+    #
+    # Changed this around 06Sep06 to support a caller-specified search-size
+    # limit. Because we ALWAYS do paged searches, we have to work around the
+    # problem that it's not legal to specify a "normal" sizelimit (in the
+    # body of the search request) that is larger than the page size we're
+    # requesting. Unfortunately, I have the feeling that this will break
+    # with LDAP servers that don't support paged searches!!!
+    #
+    # (Because we pass zero as the sizelimit on search rounds when the
+    # remaining limit is larger than our max page size of 126. In these
+    # cases, I think the caller's search limit will be ignored!)
+    #
+    # CONFIRMED: This code doesn't work on LDAPs that don't support paged
+    # searches when the size limit is larger than 126. We're going to have
+    # to do a root-DSE record search and not do a paged search if the LDAP
+    # doesn't support it. Yuck.
+    rfc2696_cookie = [126, ""]
+    result_pdu = nil
+    n_results = 0
+
+    message_id = next_msgid
+
+    instrument "search.net_ldap_connection",
+               message_id: message_id,
+               filter:     filter,
+               base:       base,
+               scope:      scope,
+               size:       size,
+               time:       time,
+               sort:       sort,
+               referrals:  refs,
+               deref:      deref,
+               attributes: attrs do |payload|
+      loop do
+        # should collect this into a private helper to clarify the structure
+        query_limit = 0
+        if size > 0
+          if paged
+            query_limit = (((size - n_results) < 126) ? (size -
+                                                              n_results) : 0)
+          else
+            query_limit = size
+          end
+        end
+
+        request = [
+          base.to_ber,
+          scope.to_ber_enumerated,
+          deref.to_ber_enumerated,
+          query_limit.to_ber, # size limit
+          time.to_ber,
+          attrs_only.to_ber,
+          filter.to_ber,
+          ber_attrs.to_ber_sequence
+        ].to_ber_appsequence(Net::LDAP::PDU::SearchRequest)
+
+        # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory
+        # this breaks when calling to_ber. (Can't force binary data to UTF-8)
+        # we have to disable paging (even though server supports it) to get around this...
+
+        controls = []
+        controls <<
+          [
+            Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,
+            # Criticality MUST be false to interoperate with normal LDAPs.
+            false.to_ber,
+            rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber
+          ].to_ber_sequence if paged
+        controls << ber_sort if ber_sort
+        controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
+
+        write(request, controls, message_id)
+
+        result_pdu = nil
+        controls = []
+
+        while pdu = queued_read(message_id)
+          case pdu.app_tag
+          when Net::LDAP::PDU::SearchReturnedData
+            n_results += 1
+            yield pdu.search_entry if block_given?
+          when Net::LDAP::PDU::SearchResultReferral
+            if refs
+              if block_given?
+                se = Net::LDAP::Entry.new
+                se[:search_referrals] = (pdu.search_referrals || [])
+                yield se
+              end
+            end
+          when Net::LDAP::PDU::SearchResult
+            result_pdu = pdu
+            controls = pdu.result_controls
+            if refs && pdu.result_code == Net::LDAP::ResultCodeReferral
+              if block_given?
+                se = Net::LDAP::Entry.new
+                se[:search_referrals] = (pdu.search_referrals || [])
+                yield se
+              end
+            end
+            break
+          else
+            raise Net::LDAP::ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}"
+          end
+        end
+
+        # count number of pages of results
+        payload[:page_count] ||= 0
+        payload[:page_count]  += 1
+
+        # When we get here, we have seen a type-5 response. If there is no
+        # error AND there is an RFC-2696 cookie, then query again for the next
+        # page of results. If not, we're done. Don't screw this up or we'll
+        # break every search we do.
+        #
+        # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't
+        # that have a parameter of AsnSyntax? Does this just accidentally
+        # work? According to RFC-2696, the value expected in this position is
+        # of type OCTET STRING, covered in the default syntax supported by
+        # read_ber, so I guess we're ok.
+        more_pages = false
+        if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls
+          controls.each do |c|
+            if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS
+              # just in case some bogus server sends us more than 1 of these.
+              more_pages = false
+              if c.value and c.value.length > 0
+                cookie = c.value.read_ber[1]
+                if cookie and cookie.length > 0
+                  rfc2696_cookie[1] = cookie
+                  more_pages = true
+                end
+              end
+            end
+          end
+        end
+
+        break unless more_pages
+      end # loop
+
+      # track total result count
+      payload[:result_count] = n_results
+
+      result_pdu || OpenStruct.new(:status => :failure, :result_code => Net::LDAP::ResultCodeOperationsError, :message => "Invalid search")
+    end # instrument
+  ensure
+
+    # clean up message queue for this search
+    messages = message_queue.delete(message_id)
+
+    # in the exceptional case some messages were *not* consumed from the queue,
+    # instrument the event but do not fail.
+    if !messages.nil? && !messages.empty?
+      instrument "search_messages_unread.net_ldap_connection",
+                 message_id: message_id, messages: messages
+    end
+  end
+
+  MODIFY_OPERATIONS = { #:nodoc:
+    :add => 0,
+    :delete => 1,
+    :replace => 2
+  }
+
+  def self.modify_ops(operations)
+    ops = []
+    if operations
+      operations.each { |op, attrib, values|
+        # TODO, fix the following line, which gives a bogus error if the
+        # opcode is invalid.
+        op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated
+        values = [ values ].flatten.map { |v| v.to_ber if v }.to_ber_set
+        values = [ attrib.to_s.to_ber, values ].to_ber_sequence
+        ops << [ op_ber, values ].to_ber
+      }
+    end
+    ops
+  end
+
+  #--
+  # TODO: need to support a time limit, in case the server fails to respond.
+  # TODO: We're throwing an exception here on empty DN. Should return a
+  # proper error instead, probaby from farther up the chain.
+  # TODO: If the user specifies a bogus opcode, we'll throw a confusing
+  # error here ("to_ber_enumerated is not defined on nil").
+  #++
+  def modify(args)
+    modify_dn = args[:dn] or raise "Unable to modify empty DN"
+    ops = self.class.modify_ops args[:operations]
+
+    message_id = next_msgid
+    request    = [
+      modify_dn.to_ber,
+      ops.to_ber_sequence
+    ].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest)
+
+    write(request, nil, message_id)
+    pdu = queued_read(message_id)
+
+    if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse
+      raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
+    end
+
+    pdu
+  end
+
+  #--
+  # TODO: need to support a time limit, in case the server fails to respond.
+  # Unlike other operation-methods in this class, we return a result hash
+  # rather than a simple result number. This is experimental, and eventually
+  # we'll want to do this with all the others. The point is to have access
+  # to the error message and the matched-DN returned by the server.
+  #++
+  def add(args)
+    add_dn = args[:dn] or raise Net::LDAP::EmptyDNError, "Unable to add empty DN"
+    add_attrs = []
+    a = args[:attributes] and a.each { |k, v|
+      add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence
+    }
+
+    message_id = next_msgid
+    request    = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(Net::LDAP::PDU::AddRequest)
+
+    write(request, nil, message_id)
+    pdu = queued_read(message_id)
+
+    if !pdu || pdu.app_tag != Net::LDAP::PDU::AddResponse
+      raise Net::LDAP::ResponseMissingError, "response missing or invalid"
+    end
+
+    pdu
+  end
+
+  #--
+  # TODO: need to support a time limit, in case the server fails to respond.
+  #++
+  def rename(args)
+    old_dn = args[:olddn] or raise "Unable to rename empty DN"
+    new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
+    delete_attrs = args[:delete_attributes] ? true : false
+    new_superior = args[:new_superior]
+
+    message_id = next_msgid
+    request    = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber]
+    request   << new_superior.to_ber_contextspecific(0) unless new_superior == nil
+
+    write(request.to_ber_appsequence(Net::LDAP::PDU::ModifyRDNRequest), nil, message_id)
+    pdu = queued_read(message_id)
+
+    if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyRDNResponse
+      raise Net::LDAP::ResponseMissingOrInvalidError.new "response missing or invalid"
+    end
+
+    pdu
+  end
+
+  #--
+  # TODO, need to support a time limit, in case the server fails to respond.
+  #++
+  def delete(args)
+    dn = args[:dn] or raise "Unable to delete empty DN"
+    controls   = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later
+    message_id = next_msgid
+    request    = dn.to_s.to_ber_application_string(Net::LDAP::PDU::DeleteRequest)
+
+    write(request, controls, message_id)
+    pdu = queued_read(message_id)
+
+    if !pdu || pdu.app_tag != Net::LDAP::PDU::DeleteResponse
+      raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
+    end
+
+    pdu
+  end
+end # class Connection
diff --git a/lib/net/ldap/dataset.rb b/lib/net/ldap/dataset.rb
index ffdee11..54fc1a0 100644
--- a/lib/net/ldap/dataset.rb
+++ b/lib/net/ldap/dataset.rb
@@ -4,11 +4,13 @@
 # to and from LDIF strings and Net::LDAP::Entry objects.
 class Net::LDAP::Dataset < Hash
   ##
-  # Dataset object comments.
-  attr_reader :comments
+  # Dataset object version, comments.
+  attr_accessor :version
+  attr_reader   :comments
 
   def initialize(*args, &block) # :nodoc:
     super
+    @version  = nil
     @comments = []
   end
 
@@ -17,6 +19,12 @@ class Net::LDAP::Dataset < Hash
   # entries.
   def to_ldif
     ary = []
+
+    if version
+      ary << "version: #{version}"
+      ary << ""
+    end
+
     ary += @comments unless @comments.empty?
     keys.sort.each do |dn|
       ary << "dn: #{dn}"
@@ -125,8 +133,14 @@ class Net::LDAP::Dataset < Hash
           if line =~ /^#/
             ds.comments << line
             yield :comment, line if block_given?
-          elsif line =~ /^dn:[\s]*/i
-            dn = $'
+          elsif line =~ /^version:[\s]*([0-9]+)$/i
+            ds.version = $1
+            yield :version, line if block_given?
+          elsif line =~ /^dn:([\:]?)[\s]*/i
+            # $1 is a colon if the dn-value is base-64 encoded
+            # $' is the dn-value
+            # Avoid the Base64 class because not all Ruby versions have it.
+            dn = ($1 == ":") ? $'.unpack('m').shift : $'
             ds[dn] = Hash.new { |k,v| k[v] = [] }
             yield :dn, dn if block_given?
           elsif line.empty?
diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb
index f27380e..c261526 100644
--- a/lib/net/ldap/entry.rb
+++ b/lib/net/ldap/entry.rb
@@ -71,7 +71,7 @@ class Net::LDAP::Entry
 
       return nil if ds.empty?
 
-      raise Net::LDAP::LdapError, "Too many LDIF entries" unless ds.size == 1
+      raise Net::LDAP::EntryOverflowError, "Too many LDIF entries" unless ds.size == 1
 
       entry = ds.to_entries.first
 
@@ -115,7 +115,7 @@ class Net::LDAP::Entry
 
   ##
   # Read the first value for the provided attribute. The attribute name
-  # is canonicalized prior to reading. Returns nil if the attribute does 
+  # is canonicalized prior to reading. Returns nil if the attribute does
   # not exist.
   def first(name)
     self[name].first
diff --git a/lib/net/ldap/error.rb b/lib/net/ldap/error.rb
new file mode 100644
index 0000000..c9a25f9
--- /dev/null
+++ b/lib/net/ldap/error.rb
@@ -0,0 +1,38 @@
+class Net::LDAP
+  class LdapError < StandardError
+    def message
+      "Deprecation warning: Net::LDAP::LdapError is no longer used. Use Net::LDAP::Error or rescue one of it's subclasses. \n" + super
+    end
+  end
+
+  class Error < StandardError; end
+
+  class AlreadyOpenedError < Error; end
+  class SocketError < Error; end
+  class ConnectionRefusedError < Error; end
+  class NoOpenSSLError < Error; end
+  class NoStartTLSResultError < Error; end
+  class NoSearchBaseError < Error; end
+  class StartTLSError < Error; end
+  class EncryptionUnsupportedError < Error; end
+  class EncMethodUnsupportedError < Error; end
+  class AuthMethodUnsupportedError < Error; end
+  class BindingInformationInvalidError < Error; end
+  class NoBindResultError < Error; end
+  class SASLChallengeOverflowError < Error; end
+  class SearchSizeInvalidError < Error; end
+  class SearchScopeInvalidError < Error; end
+  class ResponseTypeInvalidError < Error; end
+  class ResponseMissingOrInvalidError < Error; end
+  class EmptyDNError < Error; end
+  class HashTypeUnsupportedError < Error; end
+  class OperatorError < Error; end
+  class SubstringFilterError < Error; end
+  class SearchFilterError < Error; end
+  class BERInvalidError < Error; end
+  class SearchFilterTypeUnknownError < Error; end
+  class BadAttributeError < Error; end
+  class FilterTypeUnknownError < Error; end
+  class FilterSyntaxInvalidError < Error; end
+  class EntryOverflowError < Error; end
+end
diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb
index 9df345b..0ab847b 100644
--- a/lib/net/ldap/filter.rb
+++ b/lib/net/ldap/filter.rb
@@ -27,7 +27,7 @@ class Net::LDAP::Filter
 
   def initialize(op, left, right) #:nodoc:
     unless FilterTypes.include?(op)
-      raise Net::LDAP::LdapError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
+      raise Net::LDAP::OperatorError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
     end
     @op = op
     @left = left
@@ -242,7 +242,7 @@ class Net::LDAP::Filter
 
     # http://tools.ietf.org/html/rfc4515 lists these exceptions from UTF1
     # charset for filters. All of the following must be escaped in any normal
-    # string using a single backslash ('\') as escape. 
+    # string using a single backslash ('\') as escape.
     #
     ESCAPES = {
       "\0" => '00', # NUL            = %x00 ; null character
@@ -251,10 +251,10 @@ class Net::LDAP::Filter
       ')'  => '29', # RPARENS        = %x29 ; right parenthesis (")")
       '\\' => '5C', # ESC            = %x5C ; esc (or backslash) ("\")
     }
-    # Compiled character class regexp using the keys from the above hash. 
+    # Compiled character class regexp using the keys from the above hash.
     ESCAPE_RE = Regexp.new(
-      "[" + 
-      ESCAPES.keys.map { |e| Regexp.escape(e) }.join + 
+      "[" +
+      ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
       "]")
 
     ##
@@ -290,7 +290,7 @@ class Net::LDAP::Filter
         ber.last.each { |b|
           case b.ber_identifier
           when 0x80 # context-specific primitive 0, SubstringFilter "initial"
-            raise Net::LDAP::LdapError, "Unrecognized substring filter; bad initial value." if str.length > 0
+            raise Net::LDAP::SubstringFilterError, "Unrecognized substring filter; bad initial value." if str.length > 0
             str += escape(b)
           when 0x81 # context-specific primitive 0, SubstringFilter "any"
             str += "*#{escape(b)}"
@@ -309,9 +309,9 @@ class Net::LDAP::Filter
         # call to_s to get rid of the BER-identifiedness of the incoming string.
         present?(ber.to_s)
       when 0xa9 # context-specific constructed 9, "extensible comparison"
-        raise Net::LDAP::LdapError, "Invalid extensible search filter, should be at least two elements" if ber.size<2
-        
-        # Reassembles the extensible filter parts 
+        raise Net::LDAP::SearchFilterError, "Invalid extensible search filter, should be at least two elements" if ber.size < 2
+
+        # Reassembles the extensible filter parts
         # (["sn", "2.4.6.8.10", "Barbara Jones", '1'])
         type = value = dn = rule = nil
         ber.each do |element|
@@ -327,10 +327,10 @@ class Net::LDAP::Filter
         attribute << type if type
         attribute << ":#{dn}" if dn
         attribute << ":#{rule}" if rule
-        
+
         ex(attribute, value)
       else
-        raise Net::LDAP::LdapError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
+        raise Net::LDAP::BERInvalidError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
       end
     end
 
@@ -357,7 +357,7 @@ class Net::LDAP::Filter
       when 0xa3 # equalityMatch. context-specific constructed 3.
         eq(obj[0], obj[1])
       else
-        raise Net::LDAP::LdapError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
+        raise Net::LDAP::SearchFilterTypeUnknownError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
       end
     end
   end
@@ -532,7 +532,7 @@ class Net::LDAP::Filter
       seq = []
 
       unless @left =~ /^([-;\w]*)(:dn)?(:(\w+|[.\w]+))?$/
-        raise Net::LDAP::LdapError, "Bad attribute #{@left}"
+        raise Net::LDAP::BadAttributeError, "Bad attribute #{@left}"
       end
       type, dn, rule = $1, $2, $4
 
@@ -639,15 +639,14 @@ class Net::LDAP::Filter
         l = entry[@left] and l = Array(l) and l.index(@right)
       end
     else
-      raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}"
+      raise Net::LDAP::FilterTypeUnknownError, "Unknown filter type in match: #{@op}"
     end
   end
 
   ##
   # Converts escaped characters (e.g., "\\28") to unescaped characters
-  # ("(").
   def unescape(right)
-    right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
+    right.to_s.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
   end
   private :unescape
 
@@ -672,7 +671,7 @@ class Net::LDAP::Filter
     def initialize(str)
       require 'strscan' # Don't load strscan until we need it.
       @filter = parse(StringScanner.new(str))
-      raise Net::LDAP::LdapError, "Invalid filter syntax." unless @filter
+      raise Net::LDAP::FilterSyntaxInvalidError, "Invalid filter syntax." unless @filter
     end
 
     ##
@@ -753,7 +752,7 @@ class Net::LDAP::Filter
         scanner.scan(/\s*/)
         if op = scanner.scan(/<=|>=|!=|:=|=/)
           scanner.scan(/\s*/)
-          if value = scanner.scan(/(?:[-\w*.+:@=,#\$%&!'\s\xC3\x80-\xCA\xAF]|[^\x00-\x7F]|\\[a-fA-F\d]{2})+/u)
+          if value = scanner.scan(/(?:[-\[\]{}\w*.+:@=,#\$%&!'^~\s\xC3\x80-\xCA\xAF]|[^\x00-\x7F]|\\[a-fA-F\d]{2})+/u)
             # 20100313 AZ: Assumes that "(uid=george*)" is the same as
             # "(uid=george* )". The standard doesn't specify, but I can find
             # no examples that suggest otherwise.
diff --git a/lib/net/ldap/password.rb b/lib/net/ldap/password.rb
index 929f55e..28406f0 100644
--- a/lib/net/ldap/password.rb
+++ b/lib/net/ldap/password.rb
@@ -21,17 +21,17 @@ class Net::LDAP::Password
     #
     attribute_value = ""
     def generate(type, str)
-       case type
-         when :md5
-            attribute_value = '{MD5}' + Base64.encode64(Digest::MD5.digest(str)).chomp! 
-         when :sha
-            attribute_value = '{SHA}' + Base64.encode64(Digest::SHA1.digest(str)).chomp! 
-         when :ssha
-	    salt = SecureRandom.random_bytes(16)
-            attribute_value = '{SSHA}' + Base64.encode64(Digest::SHA1.digest(str + salt) + salt).chomp!
-         else
-            raise Net::LDAP::LdapError, "Unsupported password-hash type (#{type})"
-         end
+      case type
+      when :md5
+         attribute_value = '{MD5}' + Base64.encode64(Digest::MD5.digest(str)).chomp!
+      when :sha
+         attribute_value = '{SHA}' + Base64.encode64(Digest::SHA1.digest(str)).chomp!
+      when :ssha
+         salt = SecureRandom.random_bytes(16)
+         attribute_value = '{SSHA}' + Base64.encode64(Digest::SHA1.digest(str + salt) + salt).chomp!
+      else
+         raise Net::LDAP::HashTypeUnsupportedError, "Unsupported password-hash type (#{type})"
+      end
       return attribute_value
     end
   end
diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb
index 26d4f8b..f749f66 100644
--- a/lib/net/ldap/pdu.rb
+++ b/lib/net/ldap/pdu.rb
@@ -18,24 +18,48 @@ require 'ostruct'
 # well with our approach.
 #
 # Currently, we only support controls on SearchResult.
+#
+# http://tools.ietf.org/html/rfc4511#section-4.1.1
+# http://tools.ietf.org/html/rfc4511#section-4.1.9
 class Net::LDAP::PDU
   class Error < RuntimeError; end
 
-  ##
-  # This message packet is a bind request.
+  # http://tools.ietf.org/html/rfc4511#section-4.2
   BindRequest = 0
+  # http://tools.ietf.org/html/rfc4511#section-4.2.2
   BindResult = 1
+  # http://tools.ietf.org/html/rfc4511#section-4.3
   UnbindRequest = 2
+  # http://tools.ietf.org/html/rfc4511#section-4.5.1
   SearchRequest = 3
+  # http://tools.ietf.org/html/rfc4511#section-4.5.2
   SearchReturnedData = 4
   SearchResult = 5
+  # see also SearchResultReferral (19)
+  # http://tools.ietf.org/html/rfc4511#section-4.6
+  ModifyRequest  = 6
   ModifyResponse = 7
+  # http://tools.ietf.org/html/rfc4511#section-4.7
+  AddRequest = 8
   AddResponse = 9
+  # http://tools.ietf.org/html/rfc4511#section-4.8
+  DeleteRequest = 10
   DeleteResponse = 11
+  # http://tools.ietf.org/html/rfc4511#section-4.9
+  ModifyRDNRequest  = 12
   ModifyRDNResponse = 13
+  # http://tools.ietf.org/html/rfc4511#section-4.10
+  CompareRequest = 14
+  CompareResponse = 15
+  # http://tools.ietf.org/html/rfc4511#section-4.11
+  AbandonRequest = 16
+  # http://tools.ietf.org/html/rfc4511#section-4.5.2
   SearchResultReferral = 19
+  # http://tools.ietf.org/html/rfc4511#section-4.12
   ExtendedRequest = 23
   ExtendedResponse = 24
+  # unused: http://tools.ietf.org/html/rfc4511#section-4.13
+  IntermediateResponse = 25
 
   ##
   # The LDAP packet message ID.
@@ -125,7 +149,7 @@ class Net::LDAP::PDU
   end
 
   def status
-    result_code == 0 ? :success : :failure
+    Net::LDAP::ResultCodesNonError.include?(result_code) ? :success : :failure
   end
 
   def success?
@@ -152,7 +176,7 @@ class Net::LDAP::PDU
       :matchedDN => sequence[1],
       :errorMessage => sequence[2]
     }
-    parse_search_referral(sequence[3]) if @ldap_result[:resultCode] == 10
+    parse_search_referral(sequence[3]) if @ldap_result[:resultCode] == Net::LDAP::ResultCodeReferral
   end
   private :parse_ldap_result
 
diff --git a/lib/net/ldap/version.rb b/lib/net/ldap/version.rb
index 155ca49..98d557c 100644
--- a/lib/net/ldap/version.rb
+++ b/lib/net/ldap/version.rb
@@ -1,5 +1,5 @@
 module Net
   class LDAP
-    VERSION = "0.8.0"
+    VERSION = "0.11"
   end
 end
diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb
index 3f9e5fb..501df85 100644
--- a/lib/net/snmp.rb
+++ b/lib/net/snmp.rb
@@ -3,268 +3,262 @@ require 'net/ldap/version'
 
 # :stopdoc:
 module Net
-    class SNMP
-      VERSION = Net::LDAP::VERSION
+  class SNMP
+    VERSION = Net::LDAP::VERSION
+    AsnSyntax = Net::BER.compile_syntax({
+      :application => {
+        :primitive => {
+          1 => :integer,  # Counter32, (RFC2578 sec 2)
+          2 => :integer,  # Gauge32 or Unsigned32, (RFC2578 sec 2)
+          3 => :integer  # TimeTicks32, (RFC2578 sec 2)
+        },
+        :constructed => {}
+      },
+      :context_specific => {
+        :primitive => {},
+        :constructed => {
+          0 => :array,  # GetRequest PDU (RFC1157 pgh 4.1.2)
+          1 => :array,  # GetNextRequest PDU (RFC1157 pgh 4.1.3)
+          2 => :array    # GetResponse PDU (RFC1157 pgh 4.1.4)
+        }
+      }
+    })
 
-	AsnSyntax = Net::BER.compile_syntax({
-	    :application => {
-		:primitive => {
-		    1 => :integer,	# Counter32, (RFC2578 sec 2)
-		    2 => :integer,	# Gauge32 or Unsigned32, (RFC2578 sec 2)
-		    3 => :integer	# TimeTicks32, (RFC2578 sec 2)
-		},
-		:constructed => {
-		}
-	    },
-	    :context_specific => {
-		:primitive => {
-		},
-		:constructed => {
-		    0 => :array,	# GetRequest PDU (RFC1157 pgh 4.1.2)
-		    1 => :array,	# GetNextRequest PDU (RFC1157 pgh 4.1.3)
-		    2 => :array		# GetResponse PDU (RFC1157 pgh 4.1.4)
-		}
-	    }
-	})
-
-	# SNMP 32-bit counter.
-	# Defined in RFC1155 (Structure of Mangement Information), section 6.
-	# A 32-bit counter is an ASN.1 application [1] implicit unsigned integer
-	# with a range from 0 to 2^^32 - 1.
-	class Counter32
-	    def initialize value
-		@value = value
-	    end
-	    def to_ber
-		@value.to_ber_application(1)
-	    end
-	end
-
-	# SNMP 32-bit gauge.
-	# Defined in RFC1155 (Structure of Mangement Information), section 6.
-	# A 32-bit counter is an ASN.1 application [2] implicit unsigned integer.
-	# This is also indistinguishable from Unsigned32. (Need to alias them.)
-	class Gauge32
-	    def initialize value
-		@value = value
-	    end
-	    def to_ber
-		@value.to_ber_application(2)
-	    end
-	end
-
-	# SNMP 32-bit timer-ticks.
-	# Defined in RFC1155 (Structure of Mangement Information), section 6.
-	# A 32-bit counter is an ASN.1 application [3] implicit unsigned integer.
-	class TimeTicks32
-	    def initialize value
-		@value = value
-	    end
-	    def to_ber
-		@value.to_ber_application(3)
-	    end
-	end
+    # SNMP 32-bit counter.
+    # Defined in RFC1155 (Structure of Mangement Information), section 6.
+    # A 32-bit counter is an ASN.1 application [1] implicit unsigned integer
+    # with a range from 0 to 2^^32 - 1.
+    class Counter32
+      def initialize value
+        @value = value
+      end
+      def to_ber
+        @value.to_ber_application(1)
+      end
     end
 
-    class SnmpPdu
-	class Error < StandardError; end
-
-	PduTypes = [
-	    :get_request,
-	    :get_next_request,
-	    :get_response,
-	    :set_request,
-	    :trap
-	]
-	ErrorStatusCodes = { # Per RFC1157, pgh 4.1.1
-	    0 => "noError",
-	    1 => "tooBig",
-	    2 => "noSuchName",
-	    3 => "badValue",
-	    4 => "readOnly",
-	    5 => "genErr"
-	}
+    # SNMP 32-bit gauge.
+    # Defined in RFC1155 (Structure of Mangement Information), section 6.
+    # A 32-bit counter is an ASN.1 application [2] implicit unsigned integer.
+    # This is also indistinguishable from Unsigned32. (Need to alias them.)
+    class Gauge32
+      def initialize value
+        @value = value
+      end
+      def to_ber
+        @value.to_ber_application(2)
+      end
+    end
 
-	class << self
-	    def parse ber_object
-		n = new
-		n.send :parse, ber_object
-		n
-	    end
-	end
+    # SNMP 32-bit timer-ticks.
+    # Defined in RFC1155 (Structure of Mangement Information), section 6.
+    # A 32-bit counter is an ASN.1 application [3] implicit unsigned integer.
+    class TimeTicks32
+      def initialize value
+        @value = value
+      end
+      def to_ber
+        @value.to_ber_application(3)
+      end
+    end
+  end
 
-	attr_reader :version, :community, :pdu_type, :variables, :error_status
-	attr_accessor :request_id, :error_index
+  class SnmpPdu
+    class Error < StandardError; end
+    PduTypes = [
+      :get_request,
+      :get_next_request,
+      :get_response,
+      :set_request,
+      :trap
+    ]
+    ErrorStatusCodes = { # Per RFC1157, pgh 4.1.1
+      0 => "noError",
+      1 => "tooBig",
+      2 => "noSuchName",
+      3 => "badValue",
+      4 => "readOnly",
+      5 => "genErr"
+    }
 
+    class << self
+      def parse ber_object
+        n = new
+        n.send :parse, ber_object
+        n
+      end
+    end
 
-	def initialize args={}
-	    @version = args[:version] || 0
-	    @community = args[:community] || "public"
-	    @pdu_type = args[:pdu_type] # leave nil unless specified; there's no reasonable default value.
-	    @error_status = args[:error_status] || 0
-	    @error_index = args[:error_index] || 0
-	    @variables = args[:variables] || []
-	end
+    attr_reader :version, :community, :pdu_type, :variables, :error_status
+    attr_accessor :request_id, :error_index
 
-	#--
-	def parse ber_object
-	    begin
-		parse_ber_object ber_object
-	    rescue Error
-		# Pass through any SnmpPdu::Error instances
-		raise $!
-	    rescue
-		# Wrap any basic parsing error so it becomes a PDU-format error
-		raise Error.new( "snmp-pdu format error" )
-	    end
-	end
-	private :parse
+    def initialize args={}
+      @version = args[:version] || 0
+      @community = args[:community] || "public"
+      @pdu_type = args[:pdu_type] # leave nil unless specified; there's no reasonable default value.
+      @error_status = args[:error_status] || 0
+      @error_index = args[:error_index] || 0
+      @variables = args[:variables] || []
+    end
 
-	def parse_ber_object ber_object
-	    send :version=, ber_object[0].to_i
-	    send :community=, ber_object[1].to_s
+    #--
+    def parse ber_object
+      begin
+        parse_ber_object ber_object
+      rescue Error
+        # Pass through any SnmpPdu::Error instances
+        raise $!
+      rescue
+        # Wrap any basic parsing error so it becomes a PDU-format error
+        raise Error.new( "snmp-pdu format error" )
+      end
+    end
+    private :parse
 
-	    data = ber_object[2]
-	    case (app_tag = data.ber_identifier & 31)
-	    when 0
-		send :pdu_type=, :get_request
-		parse_get_request data
-	    when 1
-		send :pdu_type=, :get_next_request
-		# This PDU is identical to get-request except for the type.
-		parse_get_request data
-	    when 2
-		send :pdu_type=, :get_response
-		# This PDU is identical to get-request except for the type,
-		# the error_status and error_index values are meaningful,
-		# and the fact that the variable bindings will be non-null.
-		parse_get_response data
-	    else
-		raise Error.new( "unknown snmp-pdu type: #{app_tag}" )
-	    end
-	end
-	private :parse_ber_object
+    def parse_ber_object ber_object
+      send :version=, ber_object[0].to_i
+      send :community=, ber_object[1].to_s
 
-	#--
-	# Defined in RFC1157, pgh 4.1.2.
-	def parse_get_request data
-	    send :request_id=, data[0].to_i
-	    # data[1] is error_status, always zero.
-	    # data[2] is error_index, always zero.
-	    send :error_status=, 0
-	    send :error_index=, 0
-	    data[3].each {|n,v|
-		# A variable-binding, of which there may be several,
-		# consists of an OID and a BER null.
-		# We're ignoring the null, we might want to verify it instead.
-		unless v.is_a?(Net::BER::BerIdentifiedNull)
-		    raise Error.new(" invalid variable-binding in get-request" )
-		end
-		add_variable_binding n, nil
-	    }
-	end
-	private :parse_get_request
+      data = ber_object[2]
+      case (app_tag = data.ber_identifier & 31)
+      when 0
+        send :pdu_type=, :get_request
+        parse_get_request data
+      when 1
+        send :pdu_type=, :get_next_request
+        # This PDU is identical to get-request except for the type.
+        parse_get_request data
+      when 2
+        send :pdu_type=, :get_response
+        # This PDU is identical to get-request except for the type,
+        # the error_status and error_index values are meaningful,
+        # and the fact that the variable bindings will be non-null.
+        parse_get_response data
+      else
+        raise Error.new( "unknown snmp-pdu type: #{app_tag}" )
+      end
+    end
+    private :parse_ber_object
 
-	#--
-	# Defined in RFC1157, pgh 4.1.4
-	def parse_get_response data
-	    send :request_id=, data[0].to_i
-	    send :error_status=, data[1].to_i
-	    send :error_index=, data[2].to_i
-	    data[3].each {|n,v|
-		# A variable-binding, of which there may be several,
-		# consists of an OID and a BER null.
-		# We're ignoring the null, we might want to verify it instead.
-		add_variable_binding n, v
-	    }
-	end
-	private :parse_get_response
+    #--
+    # Defined in RFC1157, pgh 4.1.2.
+    def parse_get_request data
+      send :request_id=, data[0].to_i
+      # data[1] is error_status, always zero.
+      # data[2] is error_index, always zero.
+      send :error_status=, 0
+      send :error_index=, 0
+      data[3].each do |n,v|
+        # A variable-binding, of which there may be several,
+        # consists of an OID and a BER null.
+        # We're ignoring the null, we might want to verify it instead.
+        unless v.is_a?(Net::BER::BerIdentifiedNull)
+            raise Error.new(" invalid variable-binding in get-request" )
+        end
+        add_variable_binding n, nil
+      end
+    end
+    private :parse_get_request
 
+    #--
+    # Defined in RFC1157, pgh 4.1.4
+    def parse_get_response data
+      send :request_id=, data[0].to_i
+      send :error_status=, data[1].to_i
+      send :error_index=, data[2].to_i
+      data[3].each do |n,v|
+        # A variable-binding, of which there may be several,
+        # consists of an OID and a BER null.
+        # We're ignoring the null, we might want to verify it instead.
+        add_variable_binding n, v
+      end
+    end
+    private :parse_get_response
 
-	def version= ver
-	    unless [0,2].include?(ver)
-		raise Error.new("unknown snmp-version: #{ver}")
-	    end
-	    @version = ver
-	end
 
-	def pdu_type= t
-	    unless PduTypes.include?(t)
-		raise Error.new("unknown pdu-type: #{t}")
-	    end
-	    @pdu_type = t
-	end
+    def version= ver
+      unless [0,2].include?(ver)
+        raise Error.new("unknown snmp-version: #{ver}")
+      end
+      @version = ver
+    end
 
-	def error_status= es
-	    unless ErrorStatusCodes.has_key?(es)
-		raise Error.new("unknown error-status: #{es}")
-	    end
-	    @error_status = es
-	end
+    def pdu_type= t
+      unless PduTypes.include?(t)
+        raise Error.new("unknown pdu-type: #{t}")
+      end
+      @pdu_type = t
+    end
 
-	def community= c
-	    @community = c.to_s
-	end
+    def error_status= es
+      unless ErrorStatusCodes.has_key?(es)
+        raise Error.new("unknown error-status: #{es}")
+      end
+      @error_status = es
+    end
 
-	#--
-	# Syntactic sugar
-	def add_variable_binding name, value=nil
-	    @variables ||= []
-	    @variables << [name, value]
-	end
+    def community= c
+      @community = c.to_s
+    end
 
-	def to_ber_string
-	    [
-		version.to_ber,
-		community.to_ber,
-		pdu_to_ber_string
-	    ].to_ber_sequence
-	end
+    #--
+    # Syntactic sugar
+    def add_variable_binding name, value=nil
+      @variables ||= []
+      @variables << [name, value]
+    end
 
-	#--
-	# Helper method that returns a PDU payload in BER form,
-	# depending on the PDU type.
-	def pdu_to_ber_string
-	    case pdu_type
-	    when :get_request
-		[
-		    request_id.to_ber,
-		    error_status.to_ber,
-		    error_index.to_ber,
-		    [
-			@variables.map {|n,v|
-			    [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence
-			}
-		    ].to_ber_sequence
-		].to_ber_contextspecific(0)
-	    when :get_next_request
-		[
-		    request_id.to_ber,
-		    error_status.to_ber,
-		    error_index.to_ber,
-		    [
-			@variables.map {|n,v|
-			    [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence
-			}
-		    ].to_ber_sequence
-		].to_ber_contextspecific(1)
-	    when :get_response
-		[
-		    request_id.to_ber,
-		    error_status.to_ber,
-		    error_index.to_ber,
-		    [
-			@variables.map {|n,v|
-			    [n.to_ber_oid, v.to_ber].to_ber_sequence
-			}
-		    ].to_ber_sequence
-		].to_ber_contextspecific(2)
-	    else
-		raise Error.new( "unknown pdu-type: #{pdu_type}" )
-	    end
-	end
-	private :pdu_to_ber_string
+    def to_ber_string
+      [
+        version.to_ber,
+        community.to_ber,
+        pdu_to_ber_string,
+      ].to_ber_sequence
+    end
 
+    #--
+    # Helper method that returns a PDU payload in BER form,
+    # depending on the PDU type.
+    def pdu_to_ber_string
+      case pdu_type
+      when :get_request
+        [
+          request_id.to_ber,
+          error_status.to_ber,
+          error_index.to_ber,
+          [
+            @variables.map {|n,v|
+              [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence
+            }
+          ].to_ber_sequence
+        ].to_ber_contextspecific(0)
+      when :get_next_request
+        [
+          request_id.to_ber,
+          error_status.to_ber,
+          error_index.to_ber,
+          [
+            @variables.map {|n,v|
+              [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence
+            }
+          ].to_ber_sequence
+        ].to_ber_contextspecific(1)
+      when :get_response
+        [
+          request_id.to_ber,
+          error_status.to_ber,
+          error_index.to_ber,
+          [
+            @variables.map {|n,v|
+              [n.to_ber_oid, v.to_ber].to_ber_sequence
+            }
+          ].to_ber_sequence
+        ].to_ber_contextspecific(2)
+      else
+        raise Error.new( "unknown pdu-type: #{pdu_type}" )
+      end
     end
+    private :pdu_to_ber_string
+  end
 end
 # :startdoc:
diff --git a/metadata.yml b/metadata.yml
index 0177f7f..a482b01 100644
--- a/metadata.yml
+++ b/metadata.yml
@@ -1,7 +1,7 @@
 --- !ruby/object:Gem::Specification
 name: net-ldap
 version: !ruby/object:Gem::Version
-  version: 0.8.0
+  version: '0.11'
 platform: ruby
 authors:
 - Francis Cianfrocca
@@ -13,106 +13,50 @@ authors:
 autorequire: 
 bindir: bin
 cert_chain: []
-date: 2014-09-10 00:00:00.000000000 Z
+date: 2015-01-21 00:00:00.000000000 Z
 dependencies:
 - !ruby/object:Gem::Dependency
-  name: rdoc
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '4.0'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '4.0'
-- !ruby/object:Gem::Dependency
-  name: hoe-git
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '1'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '1'
-- !ruby/object:Gem::Dependency
-  name: hoe-gemspec
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '1'
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - "~>"
-      - !ruby/object:Gem::Version
-        version: '1'
-- !ruby/object:Gem::Dependency
-  name: metaid
+  name: flexmock
   requirement: !ruby/object:Gem::Requirement
     requirements:
     - - "~>"
       - !ruby/object:Gem::Version
-        version: '1'
+        version: '1.3'
   type: :development
   prerelease: false
   version_requirements: !ruby/object:Gem::Requirement
     requirements:
     - - "~>"
       - !ruby/object:Gem::Version
-        version: '1'
+        version: '1.3'
 - !ruby/object:Gem::Dependency
-  name: flexmock
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: 1.3.0
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: 1.3.0
-- !ruby/object:Gem::Dependency
-  name: rspec
+  name: rake
   requirement: !ruby/object:Gem::Requirement
     requirements:
     - - "~>"
       - !ruby/object:Gem::Version
-        version: '2.0'
+        version: '10.0'
   type: :development
   prerelease: false
   version_requirements: !ruby/object:Gem::Requirement
     requirements:
     - - "~>"
       - !ruby/object:Gem::Version
-        version: '2.0'
+        version: '10.0'
 - !ruby/object:Gem::Dependency
-  name: hoe
+  name: rubocop
   requirement: !ruby/object:Gem::Requirement
     requirements:
     - - "~>"
       - !ruby/object:Gem::Version
-        version: '3.10'
+        version: 0.28.0
   type: :development
   prerelease: false
   version_requirements: !ruby/object:Gem::Requirement
     requirements:
     - - "~>"
       - !ruby/object:Gem::Version
-        version: '3.10'
+        version: 0.28.0
 description: |-
   Net::LDAP for Ruby (also called net-ldap) implements client access for the
   Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for
@@ -122,16 +66,15 @@ description: |-
 
   Net::LDAP has been tested against modern popular LDAP servers including
   OpenLDAP and Active Directory. The current release is mostly compliant with
-  earlier versions of the IETF LDAP RFCs (2251–2256, 2829–2830, 3377, and 3771).
+  earlier versions of the IETF LDAP RFCs (2251-2256, 2829-2830, 3377, and 3771).
   Our roadmap for Net::LDAP 1.0 is to gain full <em>client</em> compliance with
-  the most recent LDAP RFCs (4510–4519, plus portions of 4520–4532).
+  the most recent LDAP RFCs (4510-4519, plutions of 4520-4532).
 email:
 - blackhedd at rubyforge.org
 - gemiel at gmail.com
 - rory.ocon at gmail.com
 - kaspar.schiess at absurd.li
 - austin at rubyforge.org
-- michael at schaaryworks.com
 executables: []
 extensions: []
 extra_rdoc_files:
@@ -139,36 +82,35 @@ extra_rdoc_files:
 - Hacking.rdoc
 - History.rdoc
 - License.rdoc
-- Manifest.txt
 - README.rdoc
 files:
-- ".autotest"
-- ".gemtest"
-- ".rspec"
+- ".gitignore"
+- ".rubocop.yml"
+- ".rubocop_todo.yml"
 - ".travis.yml"
+- CONTRIBUTING.md
 - Contributors.rdoc
 - Gemfile
 - Hacking.rdoc
 - History.rdoc
 - License.rdoc
-- Manifest.txt
 - README.rdoc
 - Rakefile
-- autotest/discover.rb
 - lib/net-ldap.rb
 - lib/net/ber.rb
 - lib/net/ber/ber_parser.rb
 - lib/net/ber/core_ext.rb
 - lib/net/ber/core_ext/array.rb
-- lib/net/ber/core_ext/bignum.rb
 - lib/net/ber/core_ext/false_class.rb
-- lib/net/ber/core_ext/fixnum.rb
+- lib/net/ber/core_ext/integer.rb
 - lib/net/ber/core_ext/string.rb
 - lib/net/ber/core_ext/true_class.rb
 - lib/net/ldap.rb
+- lib/net/ldap/connection.rb
 - lib/net/ldap/dataset.rb
 - lib/net/ldap/dn.rb
 - lib/net/ldap/entry.rb
+- lib/net/ldap/error.rb
 - lib/net/ldap/filter.rb
 - lib/net/ldap/instrumentation.rb
 - lib/net/ldap/password.rb
@@ -176,30 +118,44 @@ files:
 - lib/net/ldap/version.rb
 - lib/net/snmp.rb
 - net-ldap.gemspec
-- spec/integration/ssl_ber_spec.rb
-- spec/spec.opts
-- spec/spec_helper.rb
-- spec/unit/ber/ber_spec.rb
-- spec/unit/ber/core_ext/array_spec.rb
-- spec/unit/ber/core_ext/string_spec.rb
-- spec/unit/ldap/dn_spec.rb
-- spec/unit/ldap/entry_spec.rb
-- spec/unit/ldap/filter_parser_spec.rb
-- spec/unit/ldap/filter_spec.rb
-- spec/unit/ldap/search_spec.rb
-- spec/unit/ldap_spec.rb
-- test/common.rb
+- script/install-openldap
+- script/package
+- script/release
+- test/ber/core_ext/test_array.rb
+- test/ber/core_ext/test_string.rb
+- test/ber/test_ber.rb
+- test/fixtures/cacert.pem
+- test/fixtures/openldap/memberof.ldif
+- test/fixtures/openldap/retcode.ldif
+- test/fixtures/openldap/slapd.conf.ldif
+- test/fixtures/seed.ldif
+- test/integration/test_add.rb
+- test/integration/test_ber.rb
+- test/integration/test_bind.rb
+- test/integration/test_delete.rb
+- test/integration/test_open.rb
+- test/integration/test_return_codes.rb
+- test/integration/test_search.rb
+- test/support/vm/openldap/.gitignore
+- test/support/vm/openldap/README.md
+- test/support/vm/openldap/Vagrantfile
+- test/test_dn.rb
 - test/test_entry.rb
 - test/test_filter.rb
+- test/test_filter_parser.rb
+- test/test_helper.rb
+- test/test_ldap.rb
 - test/test_ldap_connection.rb
 - test/test_ldif.rb
 - test/test_password.rb
 - test/test_rename.rb
+- test/test_search.rb
 - test/test_snmp.rb
+- test/test_ssl_ber.rb
 - test/testdata.ldif
 - testserver/ldapserver.rb
 - testserver/testdata.ldif
-homepage: http://rubyldap.com/'
+homepage: http://github.com/ruby-ldap/ruby-net-ldap
 licenses:
 - MIT
 metadata: {}
@@ -213,7 +169,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
   requirements:
   - - ">="
     - !ruby/object:Gem::Version
-      version: 1.8.7
+      version: 1.9.3
 required_rubygems_version: !ruby/object:Gem::Requirement
   requirements:
   - - ">="
@@ -228,10 +184,37 @@ summary: Net::LDAP for Ruby (also called net-ldap) implements client access for
   Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for accessing
   distributed directory services
 test_files:
+- test/ber/core_ext/test_array.rb
+- test/ber/core_ext/test_string.rb
+- test/ber/test_ber.rb
+- test/fixtures/cacert.pem
+- test/fixtures/openldap/memberof.ldif
+- test/fixtures/openldap/retcode.ldif
+- test/fixtures/openldap/slapd.conf.ldif
+- test/fixtures/seed.ldif
+- test/integration/test_add.rb
+- test/integration/test_ber.rb
+- test/integration/test_bind.rb
+- test/integration/test_delete.rb
+- test/integration/test_open.rb
+- test/integration/test_return_codes.rb
+- test/integration/test_search.rb
+- test/support/vm/openldap/.gitignore
+- test/support/vm/openldap/README.md
+- test/support/vm/openldap/Vagrantfile
+- test/test_dn.rb
 - test/test_entry.rb
 - test/test_filter.rb
+- test/test_filter_parser.rb
+- test/test_helper.rb
+- test/test_ldap.rb
 - test/test_ldap_connection.rb
 - test/test_ldif.rb
 - test/test_password.rb
 - test/test_rename.rb
+- test/test_search.rb
 - test/test_snmp.rb
+- test/test_ssl_ber.rb
+- test/testdata.ldif
+- testserver/ldapserver.rb
+- testserver/testdata.ldif
diff --git a/net-ldap.gemspec b/net-ldap.gemspec
index 854d4c6..7cdd29d 100644
--- a/net-ldap.gemspec
+++ b/net-ldap.gemspec
@@ -7,10 +7,7 @@ Gem::Specification.new do |s|
   s.name = %q{net-ldap}
   s.version = Net::LDAP::VERSION
   s.license = "MIT"
-
-  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
   s.authors = ["Francis Cianfrocca", "Emiel van de Laar", "Rory O'Connell", "Kaspar Schiess", "Austin Ziegler", "Michael Schaarschmidt"]
-  s.date = %q{2012-02-28}
   s.description = %q{Net::LDAP for Ruby (also called net-ldap) implements client access for the
 Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for
 accessing distributed directory services. Net::LDAP is written completely in
@@ -23,41 +20,16 @@ earlier versions of the IETF LDAP RFCs (2251-2256, 2829-2830, 3377, and 3771).
 Our roadmap for Net::LDAP 1.0 is to gain full <em>client</em> compliance with
 the most recent LDAP RFCs (4510-4519, plutions of 4520-4532).}
   s.email = ["blackhedd at rubyforge.org", "gemiel at gmail.com", "rory.ocon at gmail.com", "kaspar.schiess at absurd.li", "austin at rubyforge.org"]
-  s.extra_rdoc_files = ["Manifest.txt", "Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "README.rdoc"]
-  s.files = [".autotest", ".rspec", "Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "Manifest.txt", "README.rdoc", "Rakefile", "autotest/discover.rb", "lib/net-ldap.rb", "lib/net/ber.rb", "lib/net/ber/ber_parser.rb", "lib/net/ber/core_ext.rb", "lib/net/ber/core_ext/array.rb", "lib/net/ber/core_ext/bignum.rb", "lib/net/ber/core_ext/false_class.rb", "lib/net/ber/core_ext/fixnum.rb", "lib/net/ber/core_ext/string.rb", "lib/net/ber/core_ext/true_class.rb", "lib/net/ldap.r [...]
+  s.extra_rdoc_files = ["Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "README.rdoc"]
+  s.files = `git ls-files`.split $/
+  s.test_files = s.files.grep(%r{^test})
   s.homepage = %q{http://github.com/ruby-ldap/ruby-net-ldap}
   s.rdoc_options = ["--main", "README.rdoc"]
   s.require_paths = ["lib"]
-  s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
-  s.rubyforge_project = %q{net-ldap}
-  s.rubygems_version = %q{1.5.2}
+  s.required_ruby_version = ">= 1.9.3"
   s.summary = %q{Net::LDAP for Ruby (also called net-ldap) implements client access for the Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for accessing distributed directory services}
-  s.test_files = ["test/test_entry.rb", "test/test_filter.rb", "test/test_ldap_connection.rb", "test/test_ldif.rb", "test/test_password.rb", "test/test_rename.rb", "test/test_snmp.rb"]
-
-  if s.respond_to? :specification_version then
-    s.specification_version = 3
 
-    if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
-      s.add_development_dependency(%q<hoe-git>, ["~> 1"])
-      s.add_development_dependency(%q<hoe-gemspec>, ["~> 1"])
-      s.add_development_dependency(%q<metaid>, ["~> 1"])
-      s.add_development_dependency(%q<flexmock>, [">= 1.3.0"])
-      s.add_development_dependency(%q<rspec>, ["~> 2.0"])
-      s.add_development_dependency(%q<hoe>, [">= 2.9.1"])
-    else
-      s.add_dependency(%q<hoe-git>, ["~> 1"])
-      s.add_dependency(%q<hoe-gemspec>, ["~> 1"])
-      s.add_dependency(%q<metaid>, ["~> 1"])
-      s.add_dependency(%q<flexmock>, [">= 1.3.0"])
-      s.add_dependency(%q<rspec>, ["~> 2.0"])
-      s.add_dependency(%q<hoe>, [">= 2.9.1"])
-    end
-  else
-    s.add_dependency(%q<hoe-git>, ["~> 1"])
-    s.add_dependency(%q<hoe-gemspec>, ["~> 1"])
-    s.add_dependency(%q<metaid>, ["~> 1"])
-    s.add_dependency(%q<flexmock>, [">= 1.3.0"])
-    s.add_dependency(%q<rspec>, ["~> 2.0"])
-    s.add_dependency(%q<hoe>, [">= 2.9.1"])
-  end
+  s.add_development_dependency("flexmock", "~> 1.3")
+  s.add_development_dependency("rake", "~> 10.0")
+  s.add_development_dependency("rubocop", "~> 0.28.0")
 end
diff --git a/script/install-openldap b/script/install-openldap
new file mode 100755
index 0000000..9547f0f
--- /dev/null
+++ b/script/install-openldap
@@ -0,0 +1,111 @@
+#!/usr/bin/env sh
+set -e
+set -x
+
+BASE_PATH="$( cd `dirname $0`/../test/fixtures/openldap && pwd )"
+SEED_PATH="$( cd `dirname $0`/../test/fixtures          && pwd )"
+
+dpkg -s slapd time ldap-utils gnutls-bin ssl-cert > /dev/null ||\
+  DEBIAN_FRONTEND=noninteractive sudo -E apt-get install -y --force-yes slapd time ldap-utils gnutls-bin ssl-cert
+
+sudo /etc/init.d/slapd stop
+
+TMPDIR=$(mktemp -d)
+cd $TMPDIR
+
+# Delete data and reconfigure.
+sudo cp -v /var/lib/ldap/DB_CONFIG ./DB_CONFIG
+sudo rm -rf /etc/ldap/slapd.d/*
+sudo rm -rf /var/lib/ldap/*
+sudo cp -v ./DB_CONFIG /var/lib/ldap/DB_CONFIG
+sudo slapadd -F /etc/ldap/slapd.d -b "cn=config" -l $BASE_PATH/slapd.conf.ldif
+# Load memberof and ref-int overlays and configure them.
+sudo slapadd -F /etc/ldap/slapd.d -b "cn=config" -l $BASE_PATH/memberof.ldif
+# Load retcode overlay and configure
+sudo slapadd -F /etc/ldap/slapd.d -b "cn=config" -l $BASE_PATH/retcode.ldif
+
+# Add base domain.
+sudo slapadd -F /etc/ldap/slapd.d <<EOM
+dn: dc=rubyldap,dc=com
+objectClass: top
+objectClass: domain
+dc: rubyldap
+EOM
+
+sudo chown -R openldap.openldap /etc/ldap/slapd.d
+sudo chown -R openldap.openldap /var/lib/ldap
+
+sudo /etc/init.d/slapd start
+
+# Import seed data.
+# NOTE: use ldapadd in order for memberOf and refint to apply, instead of:
+# cat $SEED_PATH/seed.ldif | sudo slapadd -F /etc/ldap/slapd.d
+/usr/bin/time sudo ldapadd -x -D "cn=admin,dc=rubyldap,dc=com" -w passworD1 \
+             -h localhost -p 389 \
+             -f $SEED_PATH/seed.ldif
+
+sudo rm -rf $TMPDIR
+
+# SSL
+
+sudo sh -c "certtool --generate-privkey > /etc/ssl/private/cakey.pem"
+
+sudo sh -c "cat > /etc/ssl/ca.info <<EOF
+cn = rubyldap
+ca
+cert_signing_key
+EOF"
+
+# Create the self-signed CA certificate:
+sudo certtool --generate-self-signed \
+--load-privkey /etc/ssl/private/cakey.pem \
+--template /etc/ssl/ca.info \
+--outfile /etc/ssl/certs/cacert.pem
+
+# Make a private key for the server:
+sudo certtool --generate-privkey \
+--bits 1024 \
+--outfile /etc/ssl/private/ldap01_slapd_key.pem
+
+sudo sh -c "cat > /etc/ssl/ldap01.info <<EOF
+organization = Example Company
+cn = ldap01.example.com
+tls_www_server
+encryption_key
+signing_key
+expiration_days = 3650
+EOF"
+
+# Create the server certificate
+sudo certtool --generate-certificate \
+  --load-privkey /etc/ssl/private/ldap01_slapd_key.pem \
+  --load-ca-certificate /etc/ssl/certs/cacert.pem \
+  --load-ca-privkey /etc/ssl/private/cakey.pem \
+  --template /etc/ssl/ldap01.info \
+  --outfile /etc/ssl/certs/ldap01_slapd_cert.pem
+
+sudo ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF | true
+dn: cn=config
+add: olcTLSCACertificateFile
+olcTLSCACertificateFile: /etc/ssl/certs/cacert.pem
+-
+add: olcTLSCertificateFile
+olcTLSCertificateFile: /etc/ssl/certs/ldap01_slapd_cert.pem
+-
+add: olcTLSCertificateKeyFile
+olcTLSCertificateKeyFile: /etc/ssl/private/ldap01_slapd_key.pem
+EOF
+
+# LDAP over TLS/SSL (ldaps://) is deprecated in favour of StartTLS. The latter
+# refers to an existing LDAP session (listening on TCP port 389) becoming
+# protected by TLS/SSL whereas LDAPS, like HTTPS, is a distinct
+# encrypted-from-the-start protocol that operates over TCP port 636. But we
+# enable it for testing here.
+sudo sed -i -e 's|^SLAPD_SERVICES="\(.*\)"|SLAPD_SERVICES="ldap:/// ldapi:/// ldaps:///"|' /etc/default/slapd
+
+sudo adduser openldap ssl-cert
+sudo chgrp ssl-cert /etc/ssl/private/ldap01_slapd_key.pem
+sudo chmod g+r /etc/ssl/private/ldap01_slapd_key.pem
+sudo chmod o-r /etc/ssl/private/ldap01_slapd_key.pem
+
+sudo service slapd restart
diff --git a/script/package b/script/package
new file mode 100755
index 0000000..5851400
--- /dev/null
+++ b/script/package
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+# Usage: script/package
+# Updates the gemspec and builds a new gem in the pkg directory.
+
+mkdir -p pkg
+gem build *.gemspec
+mv *.gem pkg
diff --git a/script/release b/script/release
new file mode 100755
index 0000000..6dcd8cb
--- /dev/null
+++ b/script/release
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+# Usage: script/release
+# Build the package, tag a commit, push it to origin, and then release the
+# package publicly.
+
+set -e
+
+version="$(script/package | grep Version: | awk '{print $2}')"
+[ -n "$version" ] || exit 1
+
+echo $version
+git commit --allow-empty -a -m "Release $version"
+git tag "v$version"
+git push origin
+git push origin "v$version"
+gem push pkg/*-${version}.gem
diff --git a/spec/integration/ssl_ber_spec.rb b/spec/integration/ssl_ber_spec.rb
deleted file mode 100644
index ae8900e..0000000
--- a/spec/integration/ssl_ber_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-require 'net/ldap'
-require 'timeout'
-
-describe "BER serialisation (SSL)" do
-  # Transmits str to #to and reads it back from #from. 
-  #
-  def transmit(str)
-    Timeout::timeout(1) do
-      to.write(str)
-      to.close
-
-      from.read
-    end
-  end
-    
-  attr_reader :to, :from
-  before(:each) do
-    @from, @to = IO.pipe
-    
-    # The production code operates on sockets, which do need #connect called
-    # on them to work. Pipes are more robust for this test, so we'll skip 
-    # the #connect call since it fails. 
-    flexmock(OpenSSL::SSL::SSLSocket).
-      new_instances.should_receive(:connect => nil)
-              
-    @to   = Net::LDAP::Connection.wrap_with_ssl(to)
-    @from = Net::LDAP::Connection.wrap_with_ssl(from)
-  end
-  
-  it "should transmit strings" do
-    transmit('foo').should == 'foo'
-  end 
-  it "should correctly transmit numbers" do
-    to.write 1234.to_ber
-    from.read_ber.should == 1234
-  end 
-end
\ No newline at end of file
diff --git a/spec/spec.opts b/spec/spec.opts
deleted file mode 100644
index d019bfb..0000000
--- a/spec/spec.opts
+++ /dev/null
@@ -1,2 +0,0 @@
---format specdoc
---colour
\ No newline at end of file
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
deleted file mode 100644
index 820dac3..0000000
--- a/spec/spec_helper.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'net/ldap'
-
-RSpec.configure do |config|
-  config.mock_with :flexmock
-
-  def raw_string(s)
-    # Conveniently, String#b only needs to be called when it exists
-    s.respond_to?(:b) ? s.b : s
-  end
-end
-
-class MockInstrumentationService
-  def initialize
-    @events = {}
-  end
-
-  def instrument(event, payload)
-    result = yield(payload)
-    @events[event] ||= []
-    @events[event] << [payload, result]
-    result
-  end
-
-  def subscribe(event)
-    @events[event] ||= []
-    @events[event]
-  end
-end
diff --git a/spec/unit/ber/ber_spec.rb b/spec/unit/ber/ber_spec.rb
deleted file mode 100644
index 9806cc8..0000000
--- a/spec/unit/ber/ber_spec.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-require 'spec_helper'
-
-require 'net/ber'
-require 'net/ldap'
-
-describe "BER encoding of" do
-  
-  RSpec::Matchers.define :properly_encode_and_decode do 
-    match do |given|
-      given.to_ber.read_ber.should == given
-    end
-  end
-   
-  context "arrays" do
-    it "should properly encode/decode []" do
-      [].should properly_encode_and_decode
-    end 
-    it "should properly encode/decode [1,2,3]" do
-      ary = [1,2,3]
-      encoded_ary = ary.map { |el| el.to_ber }.to_ber
-      
-      encoded_ary.read_ber.should == ary
-    end 
-  end
-  context "booleans" do
-    it "should encode true" do
-      true.to_ber.should == "\x01\x01\x01"
-    end
-    it "should encode false" do
-      false.to_ber.should == "\x01\x01\x00"
-    end
-  end
-  context "numbers" do
-    # Sample based
-    {
-      0           => raw_string("\x02\x01\x00"),
-      1           => raw_string("\x02\x01\x01"),
-      127         => raw_string("\x02\x01\x7F"),
-      128         => raw_string("\x02\x01\x80"),
-      255         => raw_string("\x02\x01\xFF"),
-      256         => raw_string("\x02\x02\x01\x00"),
-      65535       => raw_string("\x02\x02\xFF\xFF"),
-      65536       => raw_string("\x02\x03\x01\x00\x00"),
-      16_777_215  => raw_string("\x02\x03\xFF\xFF\xFF"),
-      0x01000000  => raw_string("\x02\x04\x01\x00\x00\x00"),
-      0x3FFFFFFF  => raw_string("\x02\x04\x3F\xFF\xFF\xFF"),
-      0x4FFFFFFF  => raw_string("\x02\x04\x4F\xFF\xFF\xFF"),
-
-      # Some odd samples...
-      5           => raw_string("\002\001\005"),
-      500         => raw_string("\002\002\001\364"),
-      50_000      => raw_string("\x02\x02\xC3P"),
-      5_000_000_000  => raw_string("\002\005\001*\005\362\000")
-    }.each do |number, expected_encoding|
-      it "should encode #{number} as #{expected_encoding.inspect}" do
-        number.to_ber.should == expected_encoding
-      end
-    end
-
-    # Round-trip encoding: This is mostly to be sure to cover Bignums well.
-    context "when decoding with #read_ber" do
-      it "should correctly handle powers of two" do
-        100.times do |p|
-          n = 2 << p
-          
-          n.should properly_encode_and_decode
-        end
-      end 
-      it "should correctly handle powers of ten" do
-        100.times do |p|
-          n = 5 * 10**p
-          
-          n.should properly_encode_and_decode
-        end
-      end 
-    end
-  end
-  if "Ruby 1.9".respond_to?(:encoding)
-    context "strings" do
-      it "should properly encode UTF-8 strings" do
-        "\u00e5".force_encoding("UTF-8").to_ber.should ==
-          raw_string("\x04\x02\xC3\xA5")
-      end
-      it "should properly encode strings encodable as UTF-8" do
-        "teststring".encode("US-ASCII").to_ber.should == "\x04\nteststring"
-      end
-			it "should properly encode binary data strings using to_ber_bin" do
-				# This is used for searching for GUIDs in Active Directory
-				["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").to_ber_bin.should == 
-					raw_string("\x04\x10" + "j1\xB4\xA1*\xA2zA\xAC\xA9`?'\xDDQ\x16")
-			end
-      it "should not fail on strings that can not be converted to UTF-8" do
-        error = Encoding::UndefinedConversionError
-        lambda {"\x81".to_ber }.should_not raise_exception(error)
-      end
-    end
-  end
-end
-
-describe "BER decoding of" do
-  context "numbers" do
-    it "should decode #{"\002\001\006".inspect} (6)" do
-      "\002\001\006".read_ber(Net::LDAP::AsnSyntax).should == 6
-    end
-    it "should decode #{"\004\007testing".inspect} ('testing')" do
-      "\004\007testing".read_ber(Net::LDAP::AsnSyntax).should == 'testing'
-    end
-    it "should decode an ldap bind request" do
-      "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus".
-        read_ber(Net::LDAP::AsnSyntax).should ==
-          [1, [3, "Administrator", "ad_is_bogus"]]
-    end 
-  end
-end
-
-describe Net::BER::BerIdentifiedString do
-  describe "initialize" do
-    subject { Net::BER::BerIdentifiedString.new(data) }
-
-    context "binary data" do
-      let(:data) { ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").force_encoding("ASCII-8BIT") }
-
-      its(:valid_encoding?) { should be_true }
-      specify { subject.encoding.name.should == "ASCII-8BIT" }
-    end
-
-    context "ascii data in UTF-8" do
-      let(:data) { "some text".force_encoding("UTF-8") }
-
-      its(:valid_encoding?) { should be_true }
-      specify { subject.encoding.name.should == "UTF-8" }
-    end
-
-    context "UTF-8 data in UTF-8" do
-      let(:data) { ["e4b8ad"].pack("H*").force_encoding("UTF-8") }
-      
-      its(:valid_encoding?) { should be_true }
-      specify { subject.encoding.name.should == "UTF-8" }
-    end
-  end
-end
diff --git a/spec/unit/ber/core_ext/array_spec.rb b/spec/unit/ber/core_ext/array_spec.rb
deleted file mode 100644
index c8a6b4e..0000000
--- a/spec/unit/ber/core_ext/array_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'spec_helper'
-require 'metaid'
-
-describe Array, "when extended with BER core extensions" do
-
-  it "should correctly convert a control code array" do
-    control_codes = []
-    control_codes << ['1.2.3'.to_ber, true.to_ber].to_ber_sequence
-    control_codes << ['1.7.9'.to_ber, false.to_ber].to_ber_sequence
-    control_codes = control_codes.to_ber_sequence
-    res = [['1.2.3', true],['1.7.9',false]].to_ber_control
-    res.should eq(control_codes)
-  end
-
-  it "should wrap the array in another array if a nested array is not passed" do
-    result1 = ['1.2.3', true].to_ber_control
-    result2 = [['1.2.3', true]].to_ber_control
-    result1.should eq(result2)
-  end
-
-  it "should return an empty string if an empty array is passed" do
-    [].to_ber_control.should be_empty
-  end
-end
diff --git a/spec/unit/ber/core_ext/string_spec.rb b/spec/unit/ber/core_ext/string_spec.rb
deleted file mode 100644
index ef2c498..0000000
--- a/spec/unit/ber/core_ext/string_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require 'spec_helper'
-require 'metaid'
-
-describe String, "when extended with BER core extensions" do
-  describe "<- #read_ber! (consuming read_ber method)" do
-    context "when passed an ldap bind request and some extra data" do
-      attr_reader :str, :result
-      before(:each) do
-        @str = raw_string("0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus UNCONSUMED")
-        @result = str.read_ber!(Net::LDAP::AsnSyntax)
-      end
-      
-      it "should correctly parse the ber message" do
-        result.should == [1, [3, "Administrator", "ad_is_bogus"]]
-      end 
-      it "should leave unconsumed part of message in place" do
-        str.should == " UNCONSUMED"
-      end
-
-      context "if an exception occurs during #read_ber" do
-        attr_reader :initial_value
-        before(:each) do
-          stub_exception_class = Class.new(StandardError)
-          
-          @initial_value = raw_string("0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus")
-          @str = initial_value.dup 
-
-          # Defines a string
-          io = StringIO.new(initial_value)
-          io.meta_def :read_ber do |syntax|
-            read
-            raise stub_exception_class
-          end
-          flexmock(StringIO).should_receive(:new).and_return(io)
-          
-          begin
-            str.read_ber!(Net::LDAP::AsnSyntax)            
-          rescue stub_exception_class
-            # EMPTY ON PURPOSE
-          else
-            raise "The stub code should raise an exception!"
-          end
-        end
-        
-        it "should not modify string" do
-          str.should == initial_value
-        end
-      end
-    end
-  end
-end
diff --git a/spec/unit/ldap/dn_spec.rb b/spec/unit/ldap/dn_spec.rb
deleted file mode 100644
index 8d1b585..0000000
--- a/spec/unit/ldap/dn_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-require 'spec_helper'
-require 'net/ldap/dn'
-
-describe Net::LDAP::DN do
-  describe "<- .construct" do
-    attr_reader :dn
-
-    before(:each) do
-      @dn = Net::LDAP::DN.new('cn', ',+"\\<>;', 'ou=company')
-    end
-
-    it "should construct a Net::LDAP::DN" do
-      dn.should be_an_instance_of(Net::LDAP::DN)
-    end 
-
-    it "should escape all the required characters" do
-      dn.to_s.should == 'cn=\\,\\+\\"\\\\\\<\\>\\;,ou=company'
-    end
-  end
-
-  describe "<- .to_a" do
-    context "parsing" do
-      {
-        'cn=James, ou=Company\\,\\20LLC' => ['cn','James','ou','Company, LLC'],
-        'cn =  \ James , ou  =  "Comp\28ny"  ' => ['cn',' James','ou','Comp(ny'],
-        '1.23.4=  #A3B4D5  ,ou=Company' => ['1.23.4','#A3B4D5','ou','Company'],
-      }.each do |key, value|
-        context "(#{key})" do
-          attr_reader :dn
-
-          before(:each) do
-            @dn = Net::LDAP::DN.new(key)
-          end
-
-          it "should decode into a Net::LDAP::DN" do
-            dn.should be_an_instance_of(Net::LDAP::DN)
-          end
-
-          it "should return the correct array" do
-            dn.to_a.should == value
-          end
-        end
-      end
-    end
-
-    context "parsing bad input" do
-      [
-        'cn=James,',
-        'cn=#aa aa',
-        'cn="James',
-        'cn=J\ames',
-        'cn=\\',
-        '1.2.d=Value',
-        'd1.2=Value',
-      ].each do |value|
-        context "(#{value})" do
-          attr_reader :dn
-
-          before(:each) do
-            @dn = Net::LDAP::DN.new(value)
-          end
-
-          it "should decode into a Net::LDAP::DN" do
-            dn.should be_an_instance_of(Net::LDAP::DN)
-          end
-
-          it "should raise an error on parsing" do
-            lambda { dn.to_a }.should raise_error
-          end
-        end
-      end
-    end
-  end
-
-  describe "<- .escape(str)" do
-    it "should escape ,, +, \", \\, <, >, and ;" do
-      Net::LDAP::DN.escape(',+"\\<>;').should == '\\,\\+\\"\\\\\\<\\>\\;'
-    end 
-  end
-end
diff --git a/spec/unit/ldap/entry_spec.rb b/spec/unit/ldap/entry_spec.rb
deleted file mode 100644
index e0270cb..0000000
--- a/spec/unit/ldap/entry_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require 'spec_helper'
-
-describe Net::LDAP::Entry do
-  attr_reader :entry
-  before(:each) do
-    @entry = Net::LDAP::Entry.from_single_ldif_string(
-      %Q{dn: something
-foo: foo
-barAttribute: bar
-      }
-    )
-  end
-
-  describe "entry access" do
-    it "should always respond to #dn" do
-      entry.should respond_to(:dn)
-    end 
-    
-    context "<- #foo" do
-      it "should respond_to?" do
-        entry.should respond_to(:foo)
-      end 
-      it "should return 'foo'" do
-        entry.foo.should == ['foo']
-      end
-    end
-    context "<- #Foo" do
-      it "should respond_to?" do
-        entry.should respond_to(:Foo)
-      end 
-      it "should return 'foo'" do
-        entry.foo.should == ['foo']
-      end 
-    end
-    context "<- #foo=" do
-      it "should respond_to?" do
-        entry.should respond_to(:foo=)
-      end 
-      it "should set 'foo'" do
-        entry.foo= 'bar'
-        entry.foo.should == ['bar']
-      end 
-    end
-    context "<- #fOo=" do
-      it "should return 'foo'" do
-        entry.fOo= 'bar'
-        entry.fOo.should == ['bar']
-      end 
-    end
-  end
-end
\ No newline at end of file
diff --git a/spec/unit/ldap/filter_parser_spec.rb b/spec/unit/ldap/filter_parser_spec.rb
deleted file mode 100644
index c65c615..0000000
--- a/spec/unit/ldap/filter_parser_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# encoding: utf-8
-require 'spec_helper'
-
-describe Net::LDAP::Filter::FilterParser do
-
-  describe "#parse" do
-    context "Given ASCIIs as filter string" do
-      let(:filter_string) { "(cn=name)" }
-      specify "should generate filter object" do
-        expect(Net::LDAP::Filter::FilterParser.parse(filter_string)).to be_a Net::LDAP::Filter
-      end
-    end
-    context "Given string including multibyte chars as filter string" do
-      let(:filter_string) { "(cn=名前)" }
-      specify "should generate filter object" do
-        expect(Net::LDAP::Filter::FilterParser.parse(filter_string)).to be_a Net::LDAP::Filter
-      end
-    end
-    context "Given string including colons ':'" do
-      let(:filter_string) { "(ismemberof=cn=edu:berkeley:app:calmessages:deans,ou=campus groups,dc=berkeley,dc=edu)" }
-      specify "should generate filter object" do
-        expect(Net::LDAP::Filter::FilterParser.parse(filter_string)).to be_a Net::LDAP::Filter
-      end
-    end
-  end
-end
diff --git a/spec/unit/ldap/filter_spec.rb b/spec/unit/ldap/filter_spec.rb
deleted file mode 100644
index 06fd3b8..0000000
--- a/spec/unit/ldap/filter_spec.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-require 'spec_helper'
-
-describe Net::LDAP::Filter do
-  describe "<- .ex(attr, value)" do
-    context "('foo', 'bar')" do
-      attr_reader :filter
-      before(:each) do
-        @filter = Net::LDAP::Filter.ex('foo', 'bar')
-      end
-      it "should convert to 'foo:=bar'" do
-        filter.to_s.should == '(foo:=bar)'
-      end 
-      it "should survive roundtrip via to_s/from_rfc2254" do
-        Net::LDAP::Filter.from_rfc2254(filter.to_s).should == filter
-      end 
-      it "should survive roundtrip conversion to/from ber" do
-        ber = filter.to_ber
-        Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)).should ==
-          filter
-      end 
-    end
-    context "various legal inputs" do
-      [
-        '(o:dn:=Ace Industry)', 
-        '(:dn:2.4.8.10:=Dino)', 
-        '(cn:dn:1.2.3.4.5:=John Smith)', 
-        '(sn:dn:2.4.6.8.10:=Barbara Jones)',
-        '(&(sn:dn:2.4.6.8.10:=Barbara Jones))'
-      ].each do |filter_str|
-        context "from_rfc2254(#{filter_str.inspect})" do
-          attr_reader :filter
-          before(:each) do
-            @filter = Net::LDAP::Filter.from_rfc2254(filter_str)
-          end
-
-          it "should decode into a Net::LDAP::Filter" do
-            filter.should be_an_instance_of(Net::LDAP::Filter)
-          end 
-          it "should survive roundtrip conversion to/from ber" do
-            ber = filter.to_ber
-            Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)).should ==
-              filter
-          end 
-        end
-      end
-    end
-  end
-  describe "<- .construct" do
-    it "should accept apostrophes in filters (regression)" do
-      Net::LDAP::Filter.construct("uid=O'Keefe").to_rfc2254.should == "(uid=O'Keefe)"
-    end 
-  end
-
-  describe "convenience filter constructors" do
-    def eq(attribute, value)
-      described_class.eq(attribute, value)
-    end
-    describe "<- .equals(attr, val)" do
-      it "should delegate to .eq with escaping" do
-        described_class.equals('dn', 'f*oo').should == eq('dn', 'f\2Aoo')
-      end 
-    end
-    describe "<- .begins(attr, val)" do
-      it "should delegate to .eq with escaping" do
-        described_class.begins('dn', 'f*oo').should == eq('dn', 'f\2Aoo*')
-      end 
-    end
-    describe "<- .ends(attr, val)" do
-      it "should delegate to .eq with escaping" do
-        described_class.ends('dn', 'f*oo').should == eq('dn', '*f\2Aoo')
-      end 
-    end
-    describe "<- .contains(attr, val)" do
-      it "should delegate to .eq with escaping" do
-        described_class.contains('dn', 'f*oo').should == eq('dn', '*f\2Aoo*')
-      end 
-    end
-  end
-  describe "<- .escape(str)" do
-    it "should escape nul, *, (, ) and \\" do
-      Net::LDAP::Filter.escape("\0*()\\").should == "\\00\\2A\\28\\29\\5C"
-    end 
-  end
-
-  context 'with a well-known BER string' do
-    ber = raw_string("\xa4\x2d" \
-      "\x04\x0b" "objectclass" \
-      "\x30\x1e" \
-      "\x80\x08" "foo" "*\\" "bar" \
-      "\x81\x08" "foo" "*\\" "bar" \
-      "\x82\x08" "foo" "*\\" "bar")
-
-    describe "<- .to_ber" do
-      [
-        "foo" "\\2A\\5C" "bar",
-        "foo" "\\2a\\5c" "bar",
-        "foo" "\\2A\\5c" "bar",
-        "foo" "\\2a\\5C" "bar"
-      ].each do |escaped|
-        it 'unescapes escaped characters' do
-          filter = Net::LDAP::Filter.eq("objectclass", "#{escaped}*#{escaped}*#{escaped}")
-          filter.to_ber.should == ber
-        end
-      end
-    end
-
-    describe '<- .parse_ber' do
-      it 'escapes characters' do
-        escaped = Net::LDAP::Filter.escape("foo" "*\\" "bar")
-        filter = Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax))
-        filter.to_s.should == "(objectclass=#{escaped}*#{escaped}*#{escaped})"
-      end
-    end
-  end
-end
diff --git a/spec/unit/ldap/search_spec.rb b/spec/unit/ldap/search_spec.rb
deleted file mode 100644
index 1a44f13..0000000
--- a/spec/unit/ldap/search_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# -*- ruby encoding: utf-8 -*-
-
-describe Net::LDAP, "search method" do
-  class FakeConnection
-    def search(args)
-      OpenStruct.new(:result_code => 1, :message => "error", :success? => false)
-    end
-  end
-
-  before(:each) do
-    @service = MockInstrumentationService.new
-    @connection = Net::LDAP.new :instrumentation_service => @service
-    @connection.instance_variable_set(:@open_connection, FakeConnection.new)
-  end
-
-  context "when :return_result => true" do
-    it "should return nil upon error" do
-      result_set = @connection.search(:return_result => true)
-      result_set.should be_nil
-    end
-  end
-
-  context "when :return_result => false" do
-    it "should return false upon error" do
-      result = @connection.search(:return_result => false)
-      result.should be_false
-    end
-  end
-
-  context "When :return_result is not given" do
-    it "should return nil upon error" do
-      result_set = @connection.search
-      result_set.should be_nil
-    end
-  end
-
-  context "when instrumentation_service is configured" do
-    it "should publish a search.net_ldap event" do
-      events = @service.subscribe "search.net_ldap"
-
-      @connection.search :filter => "test"
-
-      payload, result = events.pop
-      payload.should have_key(:result)
-      payload.should have_key(:filter)
-      payload[:filter].should == "test"
-    end
-  end
-end
diff --git a/spec/unit/ldap_spec.rb b/spec/unit/ldap_spec.rb
deleted file mode 100644
index af80f32..0000000
--- a/spec/unit/ldap_spec.rb
+++ /dev/null
@@ -1,223 +0,0 @@
-require 'spec_helper'
-
-describe Net::LDAP do
-  describe "initialize" do
-    context "when instrumentation is configured" do
-      before do
-        @connection = flexmock(:connection, :close => true)
-        flexmock(Net::LDAP::Connection).should_receive(:new).and_return(@connection)
-
-        @service = MockInstrumentationService.new
-      end
-
-      subject do
-        Net::LDAP.new \
-          :server => "test.mocked.com", :port => 636,
-          :force_no_page => true, # so server capabilities are not queried
-          :instrumentation_service => @service
-      end
-
-      it "should instrument bind" do
-        events = @service.subscribe "bind.net_ldap"
-
-        bind_result = flexmock(:bind_result, :success? => true)
-        @connection.should_receive(:bind).with(Hash).and_return(bind_result)
-
-        subject.bind.should be_true
-
-        payload, result = events.pop
-        result.should be_true
-        payload[:bind].should == bind_result
-      end
-
-      it "should instrument search" do
-        events = @service.subscribe "search.net_ldap"
-
-        @connection.should_receive(:bind).and_return(flexmock(:bind_result, :result_code => 0))
-        @connection.should_receive(:search).with(Hash, Proc).
-                    yields(entry = Net::LDAP::Entry.new("uid=user1,ou=users,dc=example,dc=com")).
-                    and_return(flexmock(:search_result, :success? => true, :result_code => 0))
-
-        subject.search(:filter => "(uid=user1)").should be_true
-
-        payload, result = events.pop
-        result.should == [entry]
-        payload[:result].should == [entry]
-        payload[:filter].should == "(uid=user1)"
-      end
-    end
-  end
-end
-
-describe Net::LDAP::Connection do
-  describe "initialize" do
-    context "when host is not responding" do
-      before(:each) do
-        flexmock(TCPSocket).
-          should_receive(:new).and_raise(Errno::ECONNREFUSED)
-      end
-
-      it "should raise LdapError" do
-        lambda {
-          Net::LDAP::Connection.new(
-            :server => 'test.mocked.com',
-            :port   => 636)
-        }.should raise_error(Net::LDAP::LdapError)
-      end
-    end
-    context "when host is blocking the port" do
-      before(:each) do
-        flexmock(TCPSocket).
-          should_receive(:new).and_raise(SocketError)
-      end
-
-      it "should raise LdapError" do
-        lambda {
-          Net::LDAP::Connection.new(
-            :server => 'test.mocked.com',
-            :port   => 636)
-        }.should raise_error(Net::LDAP::LdapError)
-      end
-    end
-    context "on other exceptions" do
-      before(:each) do
-        flexmock(TCPSocket).
-          should_receive(:new).and_raise(NameError)
-      end
-
-      it "should rethrow the exception" do
-        lambda {
-          Net::LDAP::Connection.new(
-            :server => 'test.mocked.com',
-            :port   => 636)
-        }.should raise_error(NameError)
-      end
-    end
-  end
-
-  context "populate error messages" do
-    before do
-      @tcp_socket = flexmock(:connection)
-      @tcp_socket.should_receive(:write)
-      flexmock(TCPSocket).should_receive(:new).and_return(@tcp_socket)
-    end
-
-    subject { Net::LDAP::Connection.new(:server => 'test.mocked.com', :port => 636) }
-
-    it "should get back error messages if operation fails" do
-      ber = Net::BER::BerIdentifiedArray.new([53, "", "The provided password value was rejected by a password validator:  The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.  The minimum number of characters from that set that must be present in user passwords is 1"])
-      ber.ber_identifier = Net::LDAP::PDU::ModifyResponse
-      @tcp_socket.should_receive(:read_ber).and_return([2, ber])
-
-      result = subject.modify(:dn => "1", :operations => [[:replace, "mail", "something at sothsdkf.com"]])
-      result.should be_failure
-      result.error_message.should == "The provided password value was rejected by a password validator:  The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.  The minimum number of characters from that set that must be present in user passwords is 1"
-    end
-
-    it "shouldn't get back error messages if operation succeeds" do
-      ber = Net::BER::BerIdentifiedArray.new([0, "", ""])
-      ber.ber_identifier = Net::LDAP::PDU::ModifyResponse
-      @tcp_socket.should_receive(:read_ber).and_return([2, ber])
-
-      result = subject.modify(:dn => "1", :operations => [[:replace, "mail", "something at sothsdkf.com"]])
-      result.should be_success
-      result.error_message.should == ""
-    end
-  end
-
-  context "instrumentation" do
-    before do
-      @tcp_socket = flexmock(:connection)
-      # handle write
-      @tcp_socket.should_receive(:write)
-      # return this mock
-      flexmock(TCPSocket).should_receive(:new).and_return(@tcp_socket)
-
-      @service = MockInstrumentationService.new
-    end
-
-    subject do
-      Net::LDAP::Connection.new(:server => 'test.mocked.com', :port => 636,
-                                :instrumentation_service => @service)
-    end
-
-    it "should publish a write.net_ldap_connection event" do
-      ber = Net::BER::BerIdentifiedArray.new([0, "", ""])
-      ber.ber_identifier = Net::LDAP::PDU::BindResult
-      read_result = [2, ber]
-      @tcp_socket.should_receive(:read_ber).and_return(read_result)
-
-      events = @service.subscribe "write.net_ldap_connection"
-
-      result = subject.bind(method: :anon)
-      result.should be_success
-
-      # a write event
-      payload, result = events.pop
-      payload.should have_key(:result)
-      payload.should have_key(:content_length)
-    end
-
-    it "should publish a read.net_ldap_connection event" do
-      ber = Net::BER::BerIdentifiedArray.new([0, "", ""])
-      ber.ber_identifier = Net::LDAP::PDU::BindResult
-      read_result = [2, ber]
-      @tcp_socket.should_receive(:read_ber).and_return(read_result)
-
-      events = @service.subscribe "read.net_ldap_connection"
-
-      result = subject.bind(method: :anon)
-      result.should be_success
-
-      # a read event
-      payload, result = events.pop
-      payload.should have_key(:result)
-      result.should == read_result
-    end
-
-    it "should publish a bind.net_ldap_connection event" do
-      ber = Net::BER::BerIdentifiedArray.new([0, "", ""])
-      ber.ber_identifier = Net::LDAP::PDU::BindResult
-      bind_result = [2, ber]
-      @tcp_socket.should_receive(:read_ber).and_return(bind_result)
-
-      events = @service.subscribe "bind.net_ldap_connection"
-
-      result = subject.bind(method: :anon)
-      result.should be_success
-
-      # a read event
-      payload, result = events.pop
-      payload.should have_key(:result)
-      result.should be_success
-    end
-
-    it "should publish a search.net_ldap_connection event" do
-      # search data
-      search_data_ber = Net::BER::BerIdentifiedArray.new([2, [
-        "uid=user1,ou=OrgUnit2,ou=OrgUnitTop,dc=openldap,dc=ghe,dc=local",
-        [ ["uid", ["user1"]] ]
-      ]])
-      search_data_ber.ber_identifier = Net::LDAP::PDU::SearchReturnedData
-      search_data = [2, search_data_ber]
-      # search result (end of results)
-      search_result_ber = Net::BER::BerIdentifiedArray.new([0, "", ""])
-      search_result_ber.ber_identifier = Net::LDAP::PDU::SearchResult
-      search_result = [2, search_result_ber]
-      @tcp_socket.should_receive(:read_ber).and_return(search_data).
-                                            and_return(search_result)
-
-      events = @service.subscribe "search.net_ldap_connection"
-
-      result = subject.search(filter: "(uid=user1)")
-      result.should be_success
-
-      # a search event
-      payload, result = events.pop
-      payload.should have_key(:result)
-      payload.should have_key(:filter)
-      payload[:filter].to_s.should == "(uid=user1)"
-      result.should be_truthy
-    end
-  end
-end
diff --git a/test/ber/core_ext/test_array.rb b/test/ber/core_ext/test_array.rb
new file mode 100644
index 0000000..308fffc
--- /dev/null
+++ b/test/ber/core_ext/test_array.rb
@@ -0,0 +1,22 @@
+require_relative '../../test_helper'
+
+class TestBERArrayExtension < Test::Unit::TestCase
+  def test_control_code_array
+    control_codes = []
+    control_codes << ['1.2.3'.to_ber, true.to_ber].to_ber_sequence
+    control_codes << ['1.7.9'.to_ber, false.to_ber].to_ber_sequence
+    control_codes = control_codes.to_ber_sequence
+    res = [['1.2.3', true],['1.7.9',false]].to_ber_control
+    assert_equal control_codes, res
+  end
+
+  def test_wrap_array_if_not_nested
+    result1 = ['1.2.3', true].to_ber_control
+    result2 = [['1.2.3', true]].to_ber_control
+    assert_equal result2, result1
+  end
+
+  def test_empty_string_if_empty_array
+    assert_equal "", [].to_ber_control
+  end
+end
diff --git a/test/ber/core_ext/test_string.rb b/test/ber/core_ext/test_string.rb
new file mode 100644
index 0000000..692770e
--- /dev/null
+++ b/test/ber/core_ext/test_string.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class TestBERStringExtension < Test::Unit::TestCase
+  def setup
+    @bind_request = "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus UNCONSUMED".b
+    @result = @bind_request.read_ber!(Net::LDAP::AsnSyntax)
+  end
+
+  def test_parse_ber
+    assert_equal [1, [3, "Administrator", "ad_is_bogus"]], @result
+  end
+
+  def test_unconsumed_message
+    assert_equal " UNCONSUMED", @bind_request
+  end
+
+  def test_exception_does_not_modify_string
+    original = "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus".b
+    duplicate = original.dup
+    flexmock(StringIO).new_instances.should_receive(:read_ber).and_raise(Net::BER::BerError)
+    duplicate.read_ber!(Net::LDAP::AsnSyntax) rescue Net::BER::BerError
+
+    assert_equal original, duplicate
+  end
+end
diff --git a/test/ber/test_ber.rb b/test/ber/test_ber.rb
new file mode 100644
index 0000000..92b3902
--- /dev/null
+++ b/test/ber/test_ber.rb
@@ -0,0 +1,145 @@
+require_relative '../test_helper'
+
+class TestBEREncoding < Test::Unit::TestCase
+  def test_empty_array
+    assert_equal [], [].to_ber.read_ber
+  end
+
+  def test_array
+    ary = [1,2,3]
+    encoded_ary = ary.map { |el| el.to_ber }.to_ber
+
+    assert_equal ary, encoded_ary.read_ber
+  end
+
+  # http://tools.ietf.org/html/rfc4511#section-5.1
+  def test_true
+    assert_equal "\x01\x01\xFF".b, true.to_ber
+  end
+
+  def test_false
+    assert_equal "\x01\x01\x00", false.to_ber
+  end
+
+  # Sample based
+  {
+    0           => "\x02\x01\x00",
+    1           => "\x02\x01\x01",
+    127         => "\x02\x01\x7F",
+    128         => "\x02\x02\x00\x80",
+    255         => "\x02\x02\x00\xFF",
+    256         => "\x02\x02\x01\x00",
+    65535       => "\x02\x03\x00\xFF\xFF",
+    65536       => "\x02\x03\x01\x00\x00",
+    8388607     => "\x02\x03\x7F\xFF\xFF",
+    8388608     => "\x02\x04\x00\x80\x00\x00",
+    16_777_215  => "\x02\x04\x00\xFF\xFF\xFF",
+    0x01000000  => "\x02\x04\x01\x00\x00\x00",
+    0x3FFFFFFF  => "\x02\x04\x3F\xFF\xFF\xFF",
+    0x4FFFFFFF  => "\x02\x04\x4F\xFF\xFF\xFF",
+
+    # Some odd samples...
+    5           => "\x02\x01\x05",
+    500         => "\x02\x02\x01\xf4",
+    50_000      => "\x02\x03\x00\xC3\x50",
+    5_000_000_000 => "\x02\x05\x01\x2a\x05\xF2\x00",
+
+    # negatives
+    -1          => "\x02\x01\xFF",
+    -127        => "\x02\x01\x81",
+    -128        => "\x02\x01\x80",
+    -255        => "\x02\x02\xFF\x01",
+    -256        => "\x02\x02\xFF\x00",
+    -65535      => "\x02\x03\xFF\x00\x01",
+    -65536      => "\x02\x03\xFF\x00\x00",
+    -65537      => "\x02\x03\xFE\xFF\xFF",
+    -8388607    => "\x02\x03\x80\x00\x01",
+    -8388608    => "\x02\x03\x80\x00\x00",
+    -16_777_215 => "\x02\x04\xFF\x00\x00\x01",
+  }.each do |number, expected_encoding|
+    define_method "test_encode_#{number}" do
+      assert_equal expected_encoding.b, number.to_ber
+    end
+
+    define_method "test_decode_encoded_#{number}" do
+      assert_equal number, expected_encoding.b.read_ber
+    end
+  end
+
+  # Round-trip encoding: This is mostly to be sure to cover Bignums well.
+  def test_powers_of_two
+    100.times do |p|
+      n = 2 << p
+
+      assert_equal n, n.to_ber.read_ber
+    end
+  end
+
+  def test_powers_of_ten
+    100.times do |p|
+      n = 5 * 10**p
+
+      assert_equal n, n.to_ber.read_ber
+    end
+  end
+
+  if "Ruby 1.9".respond_to?(:encoding)
+    def test_encode_utf8_strings
+      assert_equal "\x04\x02\xC3\xA5".b, "\u00e5".force_encoding("UTF-8").to_ber
+    end
+
+    def test_utf8_encodable_strings
+      assert_equal "\x04\nteststring", "teststring".encode("US-ASCII").to_ber
+    end
+
+    def test_encode_binary_data
+      # This is used for searching for GUIDs in Active Directory
+      assert_equal "\x04\x10" + "j1\xB4\xA1*\xA2zA\xAC\xA9`?'\xDDQ\x16".b,
+        ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").to_ber_bin
+    end
+
+    def test_non_utf8_encodable_strings
+      assert_equal "\x04\x01\x81".b, "\x81".to_ber
+    end
+  end
+end
+
+class TestBERDecoding < Test::Unit::TestCase
+  def test_decode_number
+    assert_equal 6, "\002\001\006".read_ber(Net::LDAP::AsnSyntax)
+  end
+
+  def test_decode_string
+    assert_equal "testing", "\004\007testing".read_ber(Net::LDAP::AsnSyntax)
+  end
+
+  def test_decode_ldap_bind_request
+    assert_equal [1, [3, "Administrator", "ad_is_bogus"]], "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus".read_ber(Net::LDAP::AsnSyntax)
+  end
+end
+
+class TestBERIdentifiedString < Test::Unit::TestCase
+  def test_binary_data
+    data = ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").force_encoding("ASCII-8BIT")
+    bis = Net::BER::BerIdentifiedString.new(data)
+
+    assert bis.valid_encoding?, "should be a valid encoding"
+    assert_equal "ASCII-8BIT", bis.encoding.name
+  end
+
+  def test_ascii_data_in_utf8
+    data = "some text".force_encoding("UTF-8")
+    bis = Net::BER::BerIdentifiedString.new(data)
+
+    assert bis.valid_encoding?, "should be a valid encoding"
+    assert_equal "UTF-8", bis.encoding.name
+  end
+
+  def test_ut8_data_in_utf8
+    data = ["e4b8ad"].pack("H*").force_encoding("UTF-8")
+    bis = Net::BER::BerIdentifiedString.new(data)
+
+    assert bis.valid_encoding?, "should be a valid encoding"
+    assert_equal "UTF-8", bis.encoding.name
+  end
+end
diff --git a/test/common.rb b/test/common.rb
deleted file mode 100644
index baa0688..0000000
--- a/test/common.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# Add 'lib' to load path.
-require 'test/unit'
-require 'net/ldap'
diff --git a/test/fixtures/cacert.pem b/test/fixtures/cacert.pem
new file mode 100644
index 0000000..f8b134e
--- /dev/null
+++ b/test/fixtures/cacert.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDRzCCAf+gAwIBAgIEVHpbmjANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhy
+dWJ5bGRhcDAeFw0xNDExMjkyMzQ5NDZaFw0xNTExMjkyMzQ5NDZaMBMxETAPBgNV
+BAMTCHJ1YnlsZGFwMIIBUjANBgkqhkiG9w0BAQEFAAOCAT8AMIIBOgKCATEA4pKe
+cDCNuL53fkpO/WSAS+gmMTsOs+oOK71kZlk2QT/MBz8TxC6m358qCADjnXcMVVxa
+ySQbQlVKZMkIvLNciZbiLDgC5II0NbHACNa8rqenoKRjS4J9W3OhA8EmnXn/Me+8
+uMCI9tfnKNRZYdkQZlra4I+Idn+xYfl/5q5b/7ZjPS2zY/585hFEYE+5vfOZVBSU
+3HMNSeuJvTehLv7dD7aQfXNM4cRgHXequkJQ/HLLFAO4AgJ+LJrFWpj7GWz3crgr
+9G5px4T78wJH3NQiOsG6UBXPw8c4T+Z6GAWX2l1zs1gZsaiCVbAraqK3404lL7yp
++ThbsW3ifzgNPhmjScXBLdbEDrrAKosW7kkTOGzxiMCBmNlj2SKhcztoduAtfF1f
+Fs2Jk8MRTHwO8ThD7wIDAQABo0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1UdDwEB
+/wQFAwMHBAAwHQYDVR0OBBYEFJDm67ekyFu4/Z7VcO6Vk/5pinGcMA0GCSqGSIb3
+DQEBCwUAA4IBMQDHeEPzfYRtjynpUKyrtxx/6ZVOfCLuz4eHkBZggz/pJacDCv/a
+I//W03XCk8RWq/fWVVUzvxXgPwnYcw992PLM7XW81zp6ruRUDWooYnjHZZz3bRhe
+kC4QvM2mZhcsMVmhmWWKZn81qXgVdUY1XNRhk87cuXjF/UTpEieFvWAsCUkFZkqB
+AmySCuI/FuPaauT1YAltkIlYAEIGNJGZDMf2BTVUQpXhTXeS9/AZWLNDBwiq+fwo
+YYnsr9MnBXCEmg1gVSR/Ay2AZmbYfiYtb5kU8uq2lSWAUb4LX6HZl82wo3OilrJ2
+WXl6Qf+Fcy4qqkRt4AKHjtzizpEDCOVYuuG0Zoy+QnxNXRsEzpb8ymnJFrcgYfk/
+6Lv2gWAFl5FqCZp7gBWg55eL2coT4C+mbNTF
+-----END CERTIFICATE-----
diff --git a/test/fixtures/openldap/memberof.ldif b/test/fixtures/openldap/memberof.ldif
new file mode 100644
index 0000000..dac7c6b
--- /dev/null
+++ b/test/fixtures/openldap/memberof.ldif
@@ -0,0 +1,33 @@
+dn: cn=module,cn=config
+cn: module
+objectClass: olcModuleList
+objectClass: top
+olcModulePath: /usr/lib/ldap
+olcModuleLoad: memberof.la
+
+dn: olcOverlay={0}memberof,olcDatabase={1}hdb,cn=config
+objectClass: olcConfig
+objectClass: olcMemberOf
+objectClass: olcOverlayConfig
+objectClass: top
+olcOverlay: memberof
+olcMemberOfDangling: ignore
+olcMemberOfRefInt: TRUE
+olcMemberOfGroupOC: groupOfNames
+olcMemberOfMemberAD: member
+olcMemberOfMemberOfAD: memberOf
+
+dn: cn=module,cn=config
+cn: module
+objectclass: olcModuleList
+objectclass: top
+olcmoduleload: refint.la
+olcmodulepath: /usr/lib/ldap
+
+dn: olcOverlay={1}refint,olcDatabase={1}hdb,cn=config
+objectClass: olcConfig
+objectClass: olcOverlayConfig
+objectClass: olcRefintConfig
+objectClass: top
+olcOverlay: {1}refint
+olcRefintAttribute: memberof member manager owner
diff --git a/test/fixtures/openldap/retcode.ldif b/test/fixtures/openldap/retcode.ldif
new file mode 100644
index 0000000..9faffe1
--- /dev/null
+++ b/test/fixtures/openldap/retcode.ldif
@@ -0,0 +1,76 @@
+dn: cn=module,cn=config
+cn: module
+objectClass: olcModuleList
+objectClass: top
+olcModulePath: /usr/lib/ldap
+olcModuleLoad: retcode.la
+
+# source: http://www.opensource.apple.com/source/OpenLDAP/OpenLDAP-186/OpenLDAP/tests/data/retcode.conf?txt
+
+dn: olcOverlay={2}retcode,olcDatabase={1}hdb,cn=config
+objectClass: olcConfig
+objectClass: olcRetcodeConfig
+objectClass: olcOverlayConfig
+objectClass: top
+olcOverlay: retcode
+olcRetcodeParent: ou=Retcodes,dc=rubyldap,dc=com
+olcRetcodeInDir: TRUE
+olcRetcodeSleep: 0
+olcRetcodeItem: "cn=success" 0x00
+olcRetcodeItem: "cn=success w/ delay" 0x00 sleeptime=2
+olcRetcodeItem: "cn=operationsError" 0x01
+olcRetcodeItem: "cn=protocolError" 0x02
+olcRetcodeItem: "cn=timeLimitExceeded" 0x03 op=search
+olcRetcodeItem: "cn=sizeLimitExceeded" 0x04 op=search
+olcRetcodeItem: "cn=compareFalse" 0x05 op=compare
+olcRetcodeItem: "cn=compareTrue" 0x06 op=compare
+olcRetcodeItem: "cn=authMethodNotSupported" 0x07
+olcRetcodeItem: "cn=strongAuthNotSupported" 0x07 text="same as authMethodNotSupported"
+olcRetcodeItem: "cn=strongAuthRequired" 0x08
+olcRetcodeItem: "cn=strongerAuthRequired" 0x08 text="same as strongAuthRequired"
+olcRetcodeItem: "cn=referral" 0x0a text="LDAPv3" ref="ldap://:9019"
+olcRetcodeItem: "cn=adminLimitExceeded" 0x0b text="LDAPv3"
+olcRetcodeItem: "cn=unavailableCriticalExtension" 0x0c text="LDAPv3"
+olcRetcodeItem: "cn=confidentialityRequired" 0x0d text="LDAPv3"
+olcRetcodeItem: "cn=saslBindInProgress" 0x0e text="LDAPv3"
+olcRetcodeItem: "cn=noSuchAttribute" 0x10
+olcRetcodeItem: "cn=undefinedAttributeType" 0x11
+olcRetcodeItem: "cn=inappropriateMatching" 0x12
+olcRetcodeItem: "cn=constraintViolation" 0x13
+olcRetcodeItem: "cn=attributeOrValueExists" 0x14
+olcRetcodeItem: "cn=invalidAttributeSyntax" 0x15
+olcRetcodeItem: "cn=noSuchObject" 0x20
+olcRetcodeItem: "cn=aliasProblem" 0x21
+olcRetcodeItem: "cn=invalidDNSyntax" 0x22
+olcRetcodeItem: "cn=aliasDereferencingProblem" 0x24
+olcRetcodeItem: "cn=proxyAuthzFailure" 0x2F text="LDAPv3 proxy authorization"
+olcRetcodeItem: "cn=inappropriateAuthentication" 0x30
+olcRetcodeItem: "cn=invalidCredentials" 0x31
+olcRetcodeItem: "cn=insufficientAccessRights" 0x32
+olcRetcodeItem: "cn=busy" 0x33
+olcRetcodeItem: "cn=unavailable" 0x34
+olcRetcodeItem: "cn=unwillingToPerform" 0x35
+olcRetcodeItem: "cn=loopDetect" 0x36
+olcRetcodeItem: "cn=namingViolation" 0x40
+olcRetcodeItem: "cn=objectClassViolation" 0x41
+olcRetcodeItem: "cn=notAllowedOnNonleaf" 0x42
+olcRetcodeItem: "cn=notAllowedOnRDN" 0x43
+olcRetcodeItem: "cn=entryAlreadyExists" 0x44
+olcRetcodeItem: "cn=objectClassModsProhibited" 0x45
+olcRetcodeItem: "cn=resultsTooLarge" 0x46 text="CLDAP"
+olcRetcodeItem: "cn=affectsMultipleDSAs" 0x47 text="LDAPv3"
+olcRetcodeItem: "cn=other" 0x50
+olcRetcodeItem: "cn=cupResourcesExhausted" 0x71
+olcRetcodeItem: "cn=cupSecurityViolation" 0x72
+olcRetcodeItem: "cn=cupInvalidData" 0x73
+olcRetcodeItem: "cn=cupUnsupportedScheme" 0x74
+olcRetcodeItem: "cn=cupReloadRequired" 0x75
+olcRetcodeItem: "cn=cancelled" 0x76
+olcRetcodeItem: "cn=noSuchOperation" 0x77
+olcRetcodeItem: "cn=tooLate" 0x78
+olcRetcodeItem: "cn=cannotCancel" 0x79
+olcRetcodeItem: "cn=syncRefreshRequired" 0x4100
+olcRetcodeItem: "cn=noOperation" 0x410e
+olcRetcodeItem: "cn=assertionFailed" 0x410f
+olcRetcodeItem: "cn=noReferralsFound" 0x4110
+olcRetcodeItem: "cn=cannotChain" 0x4111
diff --git a/test/fixtures/openldap/slapd.conf.ldif b/test/fixtures/openldap/slapd.conf.ldif
new file mode 100644
index 0000000..6ba5cf7
--- /dev/null
+++ b/test/fixtures/openldap/slapd.conf.ldif
@@ -0,0 +1,67 @@
+dn: cn=config
+objectClass: olcGlobal
+cn: config
+olcPidFile: /var/run/slapd/slapd.pid
+olcArgsFile: /var/run/slapd/slapd.args
+olcLogLevel: none
+olcToolThreads: 1
+
+dn: olcDatabase={-1}frontend,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcFrontendConfig
+olcDatabase: {-1}frontend
+olcSizeLimit: 500
+olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
+olcAccess: {1}to dn.exact="" by * read
+olcAccess: {2}to dn.base="cn=Subschema" by * read
+
+dn: olcDatabase=config,cn=config
+objectClass: olcDatabaseConfig
+olcDatabase: config
+olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
+
+dn: cn=schema,cn=config
+objectClass: olcSchemaConfig
+cn: schema
+
+include: file:///etc/ldap/schema/core.ldif
+include: file:///etc/ldap/schema/cosine.ldif
+include: file:///etc/ldap/schema/nis.ldif
+include: file:///etc/ldap/schema/inetorgperson.ldif
+
+dn: cn=module{0},cn=config
+objectClass: olcModuleList
+cn: module{0}
+olcModulePath: /usr/lib/ldap
+olcModuleLoad: back_hdb
+
+dn: olcBackend=hdb,cn=config
+objectClass: olcBackendConfig
+olcBackend: hdb
+
+dn: olcDatabase=hdb,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcHdbConfig
+olcDatabase: hdb
+olcDbCheckpoint: 512 30
+olcDbConfig: set_cachesize 1 0 0
+olcDbConfig: set_lk_max_objects 1500
+olcDbConfig: set_lk_max_locks 1500
+olcDbConfig: set_lk_max_lockers 1500
+olcLastMod: TRUE
+olcSuffix: dc=rubyldap,dc=com
+olcDbDirectory: /var/lib/ldap
+olcRootDN: cn=admin,dc=rubyldap,dc=com
+# admin's password: "passworD1"
+olcRootPW: {SHA}LFSkM9eegU6j3PeGG7UuHrT/KZM=
+olcDbIndex: objectClass eq
+olcAccess: to attrs=userPassword,shadowLastChange
+  by self write
+  by anonymous auth
+  by dn="cn=admin,dc=rubyldap,dc=com" write
+  by * none
+olcAccess: to dn.base="" by * read
+olcAccess: to *
+  by self write
+  by dn="cn=admin,dc=rubyldap,dc=com" write
+  by * read
diff --git a/test/fixtures/seed.ldif b/test/fixtures/seed.ldif
new file mode 100644
index 0000000..3ad3e29
--- /dev/null
+++ b/test/fixtures/seed.ldif
@@ -0,0 +1,374 @@
+dn: ou=People,dc=rubyldap,dc=com
+objectClass: top
+objectClass: organizationalUnit
+ou: People
+
+dn: ou=Groups,dc=rubyldap,dc=com
+objectClass: top
+objectClass: organizationalUnit
+ou: Groups
+
+# Directory Superuser
+dn: uid=admin,dc=rubyldap,dc=com
+uid: admin
+cn: system administrator
+sn: administrator
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+displayName: Directory Superuser
+userPassword: passworD1
+
+# Users 1-10
+
+dn: uid=user1,ou=People,dc=rubyldap,dc=com
+uid: user1
+cn: user1
+sn: user1
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: user1 at rubyldap.com
+
+dn: uid=user2,ou=People,dc=rubyldap,dc=com
+uid: user2
+cn: user2
+sn: user2
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: user2 at rubyldap.com
+
+dn: uid=user3,ou=People,dc=rubyldap,dc=com
+uid: user3
+cn: user3
+sn: user3
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: user3 at rubyldap.com
+
+dn: uid=user4,ou=People,dc=rubyldap,dc=com
+uid: user4
+cn: user4
+sn: user4
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: user4 at rubyldap.com
+
+dn: uid=user5,ou=People,dc=rubyldap,dc=com
+uid: user5
+cn: user5
+sn: user5
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: user5 at rubyldap.com
+
+dn: uid=user6,ou=People,dc=rubyldap,dc=com
+uid: user6
+cn: user6
+sn: user6
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: user6 at rubyldap.com
+
+dn: uid=user7,ou=People,dc=rubyldap,dc=com
+uid: user7
+cn: user7
+sn: user7
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: user7 at rubyldap.com
+
+dn: uid=user8,ou=People,dc=rubyldap,dc=com
+uid: user8
+cn: user8
+sn: user8
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: user8 at rubyldap.com
+
+dn: uid=user9,ou=People,dc=rubyldap,dc=com
+uid: user9
+cn: user9
+sn: user9
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: user9 at rubyldap.com
+
+dn: uid=user10,ou=People,dc=rubyldap,dc=com
+uid: user10
+cn: user10
+sn: user10
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: user10 at rubyldap.com
+
+# Emailless User
+
+dn: uid=emailless-user1,ou=People,dc=rubyldap,dc=com
+uid: emailless-user1
+cn: emailless-user1
+sn: emailless-user1
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+
+# Groupless User
+
+dn: uid=groupless-user1,ou=People,dc=rubyldap,dc=com
+uid: groupless-user1
+cn: groupless-user1
+sn: groupless-user1
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+
+# Admin User
+
+dn: uid=admin1,ou=People,dc=rubyldap,dc=com
+uid: admin1
+cn: admin1
+sn: admin1
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+userPassword: passworD1
+mail: admin1 at rubyldap.com
+
+# Groups
+
+dn: cn=ghe-users,ou=Groups,dc=rubyldap,dc=com
+cn: ghe-users
+objectClass: groupOfNames
+member: uid=user1,ou=People,dc=rubyldap,dc=com
+member: uid=emailless-user1,ou=People,dc=rubyldap,dc=com
+
+dn: cn=all-users,ou=Groups,dc=rubyldap,dc=com
+cn: all-users
+objectClass: groupOfNames
+member: cn=ghe-users,ou=Groups,dc=rubyldap,dc=com
+member: uid=user1,ou=People,dc=rubyldap,dc=com
+member: uid=user2,ou=People,dc=rubyldap,dc=com
+member: uid=user3,ou=People,dc=rubyldap,dc=com
+member: uid=user4,ou=People,dc=rubyldap,dc=com
+member: uid=user5,ou=People,dc=rubyldap,dc=com
+member: uid=user6,ou=People,dc=rubyldap,dc=com
+member: uid=user7,ou=People,dc=rubyldap,dc=com
+member: uid=user8,ou=People,dc=rubyldap,dc=com
+member: uid=user9,ou=People,dc=rubyldap,dc=com
+member: uid=user10,ou=People,dc=rubyldap,dc=com
+member: uid=emailless-user1,ou=People,dc=rubyldap,dc=com
+
+dn: cn=ghe-admins,ou=Groups,dc=rubyldap,dc=com
+cn: ghe-admins
+objectClass: groupOfNames
+member: uid=admin1,ou=People,dc=rubyldap,dc=com
+
+dn: cn=all-admins,ou=Groups,dc=rubyldap,dc=com
+cn: all-admins
+objectClass: groupOfNames
+member: cn=ghe-admins,ou=Groups,dc=rubyldap,dc=com
+member: uid=admin1,ou=People,dc=rubyldap,dc=com
+
+dn: cn=n-member-group10,ou=Groups,dc=rubyldap,dc=com
+cn: n-member-group10
+objectClass: groupOfNames
+member: uid=user1,ou=People,dc=rubyldap,dc=com
+member: uid=user2,ou=People,dc=rubyldap,dc=com
+member: uid=user3,ou=People,dc=rubyldap,dc=com
+member: uid=user4,ou=People,dc=rubyldap,dc=com
+member: uid=user5,ou=People,dc=rubyldap,dc=com
+member: uid=user6,ou=People,dc=rubyldap,dc=com
+member: uid=user7,ou=People,dc=rubyldap,dc=com
+member: uid=user8,ou=People,dc=rubyldap,dc=com
+member: uid=user9,ou=People,dc=rubyldap,dc=com
+member: uid=user10,ou=People,dc=rubyldap,dc=com
+
+dn: cn=nested-group1,ou=Groups,dc=rubyldap,dc=com
+cn: nested-group1
+objectClass: groupOfNames
+member: uid=user1,ou=People,dc=rubyldap,dc=com
+member: uid=user2,ou=People,dc=rubyldap,dc=com
+member: uid=user3,ou=People,dc=rubyldap,dc=com
+member: uid=user4,ou=People,dc=rubyldap,dc=com
+member: uid=user5,ou=People,dc=rubyldap,dc=com
+
+dn: cn=nested-group2,ou=Groups,dc=rubyldap,dc=com
+cn: nested-group2
+objectClass: groupOfNames
+member: uid=user6,ou=People,dc=rubyldap,dc=com
+member: uid=user7,ou=People,dc=rubyldap,dc=com
+member: uid=user8,ou=People,dc=rubyldap,dc=com
+member: uid=user9,ou=People,dc=rubyldap,dc=com
+member: uid=user10,ou=People,dc=rubyldap,dc=com
+
+dn: cn=nested-groups,ou=Groups,dc=rubyldap,dc=com
+cn: nested-groups
+objectClass: groupOfNames
+member: cn=nested-group1,ou=Groups,dc=rubyldap,dc=com
+member: cn=nested-group2,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=n-member-nested-group1,ou=Groups,dc=rubyldap,dc=com
+cn: n-member-nested-group1
+objectClass: groupOfNames
+member: cn=nested-group1,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=deeply-nested-group0.0.0,ou=Groups,dc=rubyldap,dc=com
+cn: deeply-nested-group0.0.0
+objectClass: groupOfNames
+member: uid=user1,ou=People,dc=rubyldap,dc=com
+member: uid=user2,ou=People,dc=rubyldap,dc=com
+member: uid=user3,ou=People,dc=rubyldap,dc=com
+member: uid=user4,ou=People,dc=rubyldap,dc=com
+member: uid=user5,ou=People,dc=rubyldap,dc=com
+
+dn: cn=deeply-nested-group0.0.1,ou=Groups,dc=rubyldap,dc=com
+cn: deeply-nested-group0.0.1
+objectClass: groupOfNames
+member: uid=user6,ou=People,dc=rubyldap,dc=com
+member: uid=user7,ou=People,dc=rubyldap,dc=com
+member: uid=user8,ou=People,dc=rubyldap,dc=com
+member: uid=user9,ou=People,dc=rubyldap,dc=com
+member: uid=user10,ou=People,dc=rubyldap,dc=com
+
+dn: cn=deeply-nested-group0.0,ou=Groups,dc=rubyldap,dc=com
+cn: deeply-nested-group0.0
+objectClass: groupOfNames
+member: cn=deeply-nested-group0.0.0,ou=Groups,dc=rubyldap,dc=com
+member: cn=deeply-nested-group0.0.1,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=deeply-nested-group0,ou=Groups,dc=rubyldap,dc=com
+cn: deeply-nested-group0
+objectClass: groupOfNames
+member: cn=deeply-nested-group0.0,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=deeply-nested-groups,ou=Groups,dc=rubyldap,dc=com
+cn: deeply-nested-groups
+objectClass: groupOfNames
+member: cn=deeply-nested-group0,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=n-depth-nested-group1,ou=Groups,dc=rubyldap,dc=com
+cn: n-depth-nested-group1
+objectClass: groupOfNames
+member: cn=nested-group1,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=n-depth-nested-group2,ou=Groups,dc=rubyldap,dc=com
+cn: n-depth-nested-group2
+objectClass: groupOfNames
+member: cn=n-depth-nested-group1,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=n-depth-nested-group3,ou=Groups,dc=rubyldap,dc=com
+cn: n-depth-nested-group3
+objectClass: groupOfNames
+member: cn=n-depth-nested-group2,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=n-depth-nested-group4,ou=Groups,dc=rubyldap,dc=com
+cn: n-depth-nested-group4
+objectClass: groupOfNames
+member: cn=n-depth-nested-group3,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=n-depth-nested-group5,ou=Groups,dc=rubyldap,dc=com
+cn: n-depth-nested-group5
+objectClass: groupOfNames
+member: cn=n-depth-nested-group4,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=n-depth-nested-group6,ou=Groups,dc=rubyldap,dc=com
+cn: n-depth-nested-group6
+objectClass: groupOfNames
+member: cn=n-depth-nested-group5,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=n-depth-nested-group7,ou=Groups,dc=rubyldap,dc=com
+cn: n-depth-nested-group7
+objectClass: groupOfNames
+member: cn=n-depth-nested-group6,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=n-depth-nested-group8,ou=Groups,dc=rubyldap,dc=com
+cn: n-depth-nested-group8
+objectClass: groupOfNames
+member: cn=n-depth-nested-group7,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=n-depth-nested-group9,ou=Groups,dc=rubyldap,dc=com
+cn: n-depth-nested-group9
+objectClass: groupOfNames
+member: cn=n-depth-nested-group8,ou=Groups,dc=rubyldap,dc=com
+
+dn: cn=head-group,ou=Groups,dc=rubyldap,dc=com
+cn: head-group
+objectClass: groupOfNames
+member: cn=tail-group,ou=Groups,dc=rubyldap,dc=com
+member: uid=user1,ou=People,dc=rubyldap,dc=com
+member: uid=user2,ou=People,dc=rubyldap,dc=com
+member: uid=user3,ou=People,dc=rubyldap,dc=com
+member: uid=user4,ou=People,dc=rubyldap,dc=com
+member: uid=user5,ou=People,dc=rubyldap,dc=com
+
+dn: cn=tail-group,ou=Groups,dc=rubyldap,dc=com
+cn: tail-group
+objectClass: groupOfNames
+member: cn=head-group,ou=Groups,dc=rubyldap,dc=com
+member: uid=user6,ou=People,dc=rubyldap,dc=com
+member: uid=user7,ou=People,dc=rubyldap,dc=com
+member: uid=user8,ou=People,dc=rubyldap,dc=com
+member: uid=user9,ou=People,dc=rubyldap,dc=com
+member: uid=user10,ou=People,dc=rubyldap,dc=com
+
+dn: cn=recursively-nested-groups,ou=Groups,dc=rubyldap,dc=com
+cn: recursively-nested-groups
+objectClass: groupOfNames
+member: cn=head-group,ou=Groups,dc=rubyldap,dc=com
+member: cn=tail-group,ou=Groups,dc=rubyldap,dc=com
+
+# posixGroup
+
+dn: cn=posix-group1,ou=Groups,dc=rubyldap,dc=com
+cn: posix-group1
+objectClass: posixGroup
+gidNumber: 1001
+memberUid: user1
+memberUid: user2
+memberUid: user3
+memberUid: user4
+memberUid: user5
+
+# missing members
+
+dn: cn=missing-users,ou=Groups,dc=rubyldap,dc=com
+cn: missing-users
+objectClass: groupOfNames
+member: uid=user1,ou=People,dc=rubyldap,dc=com
+member: uid=user2,ou=People,dc=rubyldap,dc=com
+member: uid=nonexistent-user,ou=People,dc=rubyldap,dc=com
diff --git a/test/integration/test_add.rb b/test/integration/test_add.rb
new file mode 100644
index 0000000..3cddb18
--- /dev/null
+++ b/test/integration/test_add.rb
@@ -0,0 +1,28 @@
+require_relative '../test_helper'
+
+class TestAddIntegration < LDAPIntegrationTestCase
+  def setup
+    super
+    @ldap.authenticate "cn=admin,dc=rubyldap,dc=com", "passworD1"
+
+    @dn = "uid=added-user1,ou=People,dc=rubyldap,dc=com"
+  end
+
+  def test_add
+    attrs = {
+      objectclass: %w(top inetOrgPerson organizationalPerson person),
+      uid:  "added-user1",
+      cn:   "added-user1",
+      sn:   "added-user1",
+      mail: "added-user1 at rubyldap.com"
+    }
+
+    assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect
+
+    assert result = @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject).first
+  end
+
+  def teardown
+    @ldap.delete dn: @dn
+  end
+end
diff --git a/test/integration/test_ber.rb b/test/integration/test_ber.rb
new file mode 100644
index 0000000..8fb4d37
--- /dev/null
+++ b/test/integration/test_ber.rb
@@ -0,0 +1,30 @@
+require_relative '../test_helper'
+
+class TestBERIntegration < LDAPIntegrationTestCase
+  # Test whether the TRUE boolean value is encoded correctly by performing a
+  # search operation.
+  def test_true_ber_encoding
+    # request these attrs to simplify test; use symbols to match Entry#attribute_names
+    attrs = [:dn, :uid, :cn, :mail]
+
+    assert types_entry = @ldap.search(
+      base: "dc=rubyldap,dc=com",
+      filter: "(uid=user1)",
+      size: 1,
+      attributes: attrs,
+      attributes_only: true
+    ).first
+
+    # matches attributes we requested
+    assert_equal attrs, types_entry.attribute_names
+
+    # assert values are empty
+    types_entry.each do |name, values|
+      next if name == :dn
+      assert values.empty?
+    end
+
+    assert_includes Net::LDAP::ResultCodesSearchSuccess,
+      @ldap.get_operation_result.code, "should be a successful search operation"
+  end
+end
diff --git a/test/integration/test_bind.rb b/test/integration/test_bind.rb
new file mode 100644
index 0000000..bea6b03
--- /dev/null
+++ b/test/integration/test_bind.rb
@@ -0,0 +1,34 @@
+require_relative '../test_helper'
+
+class TestBindIntegration < LDAPIntegrationTestCase
+  def test_bind_success
+    assert @ldap.bind(method: :simple, username: "uid=user1,ou=People,dc=rubyldap,dc=com", password: "passworD1"), @ldap.get_operation_result.inspect
+  end
+
+  def test_bind_anonymous_fail
+    refute @ldap.bind(method: :simple, username: "uid=user1,ou=People,dc=rubyldap,dc=com", password: ""), @ldap.get_operation_result.inspect
+
+    result = @ldap.get_operation_result
+    assert_equal Net::LDAP::ResultCodeUnwillingToPerform, result.code
+    assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeUnwillingToPerform], result.message
+    assert_equal "unauthenticated bind (DN with no password) disallowed",
+      result.error_message
+    assert_equal "", result.matched_dn
+  end
+
+  def test_bind_fail
+    refute @ldap.bind(method: :simple, username: "uid=user1,ou=People,dc=rubyldap,dc=com", password: "not my password"), @ldap.get_operation_result.inspect
+  end
+
+  def test_bind_tls_with_cafile
+    tls_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.merge(:ca_file => CA_FILE)
+    @ldap.encryption(method: :start_tls, tls_options: tls_options)
+    assert @ldap.bind(method: :simple, username: "uid=user1,ou=People,dc=rubyldap,dc=com", password: "passworD1"), @ldap.get_operation_result.inspect
+  end
+
+  def test_bind_tls_with_verify_none
+    tls_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.merge(:verify_mode => OpenSSL::SSL::VERIFY_NONE)
+    @ldap.encryption(method: :start_tls, tls_options: tls_options)
+    assert @ldap.bind(method: :simple, username: "uid=user1,ou=People,dc=rubyldap,dc=com", password: "passworD1"), @ldap.get_operation_result.inspect
+  end
+end
diff --git a/test/integration/test_delete.rb b/test/integration/test_delete.rb
new file mode 100644
index 0000000..355df7b
--- /dev/null
+++ b/test/integration/test_delete.rb
@@ -0,0 +1,31 @@
+require_relative '../test_helper'
+
+class TestDeleteIntegration < LDAPIntegrationTestCase
+  def setup
+    super
+    @ldap.authenticate "cn=admin,dc=rubyldap,dc=com", "passworD1"
+
+    @dn = "uid=delete-user1,ou=People,dc=rubyldap,dc=com"
+
+    attrs = {
+      objectclass: %w(top inetOrgPerson organizationalPerson person),
+      uid:  "delete-user1",
+      cn:   "delete-user1",
+      sn:   "delete-user1",
+      mail: "delete-user1 at rubyldap.com"
+    }
+    unless @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject)
+      assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect
+    end
+    assert @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject)
+  end
+
+  def test_delete
+    assert @ldap.delete(dn: @dn), @ldap.get_operation_result.inspect
+    refute @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject)
+
+    result = @ldap.get_operation_result
+    assert_equal Net::LDAP::ResultCodeNoSuchObject, result.code
+    assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeNoSuchObject], result.message
+  end
+end
diff --git a/test/integration/test_open.rb b/test/integration/test_open.rb
new file mode 100644
index 0000000..36724f5
--- /dev/null
+++ b/test/integration/test_open.rb
@@ -0,0 +1,88 @@
+require_relative '../test_helper'
+
+class TestBindIntegration < LDAPIntegrationTestCase
+  def test_binds_without_open
+    events = @service.subscribe "bind.net_ldap_connection"
+
+    @ldap.search(filter: "uid=user1", base: "ou=People,dc=rubyldap,dc=com", ignore_server_caps: true)
+    @ldap.search(filter: "uid=user1", base: "ou=People,dc=rubyldap,dc=com", ignore_server_caps: true)
+
+    assert_equal 2, events.size
+  end
+
+  def test_binds_with_open
+    events = @service.subscribe "bind.net_ldap_connection"
+
+    @ldap.open do
+      @ldap.search(filter: "uid=user1", base: "ou=People,dc=rubyldap,dc=com", ignore_server_caps: true)
+      @ldap.search(filter: "uid=user1", base: "ou=People,dc=rubyldap,dc=com", ignore_server_caps: true)
+    end
+
+    assert_equal 1, events.size
+  end
+
+  # NOTE: query for two or more entries so that the socket must be read
+  # multiple times.
+  # See The Problem: https://github.com/ruby-ldap/ruby-net-ldap/issues/136
+
+  def test_nested_search_without_open
+    entries = []
+    nested_entry = nil
+
+    @ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=rubyldap,dc=com") do |entry|
+      entries << entry.uid.first
+      nested_entry ||= @ldap.search(filter: "uid=user3", base: "ou=People,dc=rubyldap,dc=com").first
+    end
+
+    assert_equal "user3", nested_entry.uid.first
+    assert_equal %w(user1 user2), entries
+  end
+
+  def test_nested_search_with_open
+    entries = []
+    nested_entry = nil
+
+    @ldap.open do
+      @ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=rubyldap,dc=com") do |entry|
+        entries << entry.uid.first
+        nested_entry ||= @ldap.search(filter: "uid=user3", base: "ou=People,dc=rubyldap,dc=com").first
+      end
+    end
+
+    assert_equal "user3", nested_entry.uid.first
+    assert_equal %w(user1 user2), entries
+  end
+
+  def test_nested_add_with_open
+    entries = []
+    nested_entry = nil
+
+    dn = "uid=nested-open-added-user1,ou=People,dc=rubyldap,dc=com"
+    attrs = {
+      objectclass: %w(top inetOrgPerson organizationalPerson person),
+      uid:  "nested-open-added-user1",
+      cn:   "nested-open-added-user1",
+      sn:   "nested-open-added-user1",
+      mail: "nested-open-added-user1 at rubyldap.com"
+    }
+
+    @ldap.authenticate "cn=admin,dc=rubyldap,dc=com", "passworD1"
+    @ldap.delete dn: dn
+
+    @ldap.open do
+      @ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=rubyldap,dc=com") do |entry|
+        entries << entry.uid.first
+
+        nested_entry ||= begin
+          assert @ldap.add(dn: dn, attributes: attrs), @ldap.get_operation_result.inspect
+          @ldap.search(base: dn, scope: Net::LDAP::SearchScope_BaseObject).first
+        end
+      end
+    end
+
+    assert_equal %w(user1 user2), entries
+    assert_equal "nested-open-added-user1", nested_entry.uid.first
+  ensure
+    @ldap.delete dn: dn
+  end
+end
diff --git a/test/integration/test_return_codes.rb b/test/integration/test_return_codes.rb
new file mode 100644
index 0000000..0e381a0
--- /dev/null
+++ b/test/integration/test_return_codes.rb
@@ -0,0 +1,38 @@
+require_relative '../test_helper'
+
+# NOTE: These tests depend on the OpenLDAP retcode overlay.
+# See: section 12.12 http://www.openldap.org/doc/admin24/overlays.html
+
+class TestReturnCodeIntegration < LDAPIntegrationTestCase
+  def test_operations_error
+    refute @ldap.search(filter: "cn=operationsError", base: "ou=Retcodes,dc=rubyldap,dc=com")
+    assert result = @ldap.get_operation_result
+
+    assert_equal Net::LDAP::ResultCodeOperationsError, result.code
+    assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeOperationsError], result.message
+  end
+
+  def test_protocol_error
+    refute @ldap.search(filter: "cn=protocolError", base: "ou=Retcodes,dc=rubyldap,dc=com")
+    assert result = @ldap.get_operation_result
+
+    assert_equal Net::LDAP::ResultCodeProtocolError, result.code
+    assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeProtocolError], result.message
+  end
+
+  def test_time_limit_exceeded
+    assert @ldap.search(filter: "cn=timeLimitExceeded", base: "ou=Retcodes,dc=rubyldap,dc=com")
+    assert result = @ldap.get_operation_result
+
+    assert_equal Net::LDAP::ResultCodeTimeLimitExceeded, result.code
+    assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeTimeLimitExceeded], result.message
+  end
+
+  def test_size_limit_exceeded
+    assert @ldap.search(filter: "cn=sizeLimitExceeded", base: "ou=Retcodes,dc=rubyldap,dc=com")
+    assert result = @ldap.get_operation_result
+
+    assert_equal Net::LDAP::ResultCodeSizeLimitExceeded, result.code
+    assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeSizeLimitExceeded], result.message
+  end
+end
diff --git a/test/integration/test_search.rb b/test/integration/test_search.rb
new file mode 100644
index 0000000..b56052c
--- /dev/null
+++ b/test/integration/test_search.rb
@@ -0,0 +1,77 @@
+require_relative '../test_helper'
+
+class TestSearchIntegration < LDAPIntegrationTestCase
+  def test_search
+    entries = []
+
+    result = @ldap.search(base: "dc=rubyldap,dc=com") do |entry|
+      assert_kind_of Net::LDAP::Entry, entry
+      entries << entry
+    end
+
+    refute entries.empty?
+    assert_equal entries, result
+  end
+
+  def test_search_without_result
+    entries = []
+
+    result = @ldap.search(base: "dc=rubyldap,dc=com", return_result: false) do |entry|
+      assert_kind_of Net::LDAP::Entry, entry
+      entries << entry
+    end
+
+    assert result
+    refute_equal entries, result
+  end
+
+  def test_search_filter_string
+    entries = @ldap.search(base: "dc=rubyldap,dc=com", filter: "(uid=user1)")
+    assert_equal 1, entries.size
+  end
+
+  def test_search_filter_object
+    filter = Net::LDAP::Filter.eq("uid", "user1") | Net::LDAP::Filter.eq("uid", "user2")
+    entries = @ldap.search(base: "dc=rubyldap,dc=com", filter: filter)
+    assert_equal 2, entries.size
+  end
+
+  def test_search_constrained_attributes
+    entry = @ldap.search(base: "uid=user1,ou=People,dc=rubyldap,dc=com", attributes: ["cn", "sn"]).first
+    assert_equal [:cn, :dn, :sn], entry.attribute_names.sort  # :dn is always included
+    assert_empty entry[:mail]
+  end
+
+  def test_search_attributes_only
+    entry = @ldap.search(base: "uid=user1,ou=People,dc=rubyldap,dc=com", attributes_only: true).first
+
+    assert_empty entry[:cn], "unexpected attribute value: #{entry[:cn]}"
+  end
+
+  def test_search_timeout
+    entries = []
+    events = @service.subscribe "search.net_ldap_connection"
+
+    result = @ldap.search(base: "dc=rubyldap,dc=com", time: 5) do |entry|
+      assert_kind_of Net::LDAP::Entry, entry
+      entries << entry
+    end
+
+    payload, _ = events.pop
+    assert_equal 5, payload[:time]
+    assert_equal entries, result
+  end
+
+  # http://tools.ietf.org/html/rfc4511#section-4.5.1.4
+  def test_search_with_size
+    entries = []
+
+    result = @ldap.search(base: "dc=rubyldap,dc=com", size: 1) do |entry|
+      assert_kind_of Net::LDAP::Entry, entry
+      entries << entry
+    end
+
+    assert_equal 1, result.size
+    assert_equal entries, result
+  end
+end
diff --git a/test/support/vm/openldap/.gitignore b/test/support/vm/openldap/.gitignore
new file mode 100644
index 0000000..dace708
--- /dev/null
+++ b/test/support/vm/openldap/.gitignore
@@ -0,0 +1 @@
+/.vagrant
diff --git a/test/support/vm/openldap/README.md b/test/support/vm/openldap/README.md
new file mode 100644
index 0000000..a276956
--- /dev/null
+++ b/test/support/vm/openldap/README.md
@@ -0,0 +1,32 @@
+# Local OpenLDAP Integration Testing
+
+Set up a [Vagrant](http://www.vagrantup.com/) VM to run integration tests against OpenLDAP locally.
+
+To run integration tests locally:
+
+``` bash
+# start VM (from the correct directory)
+$ cd test/support/vm/openldap/
+$ vagrant up
+
+# get the IP address of the VM
+$ ip=$(vagrant ssh -- "ifconfig eth1 | grep -o -E '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1")
+
+# change back to root project directory
+$ cd ../../../..
+
+# run all tests, including integration tests
+$ time INTEGRATION=openldap INTEGRATION_HOST=$ip bundle exec rake
+
+# run a specific integration test file
+$ time INTEGRATION=openldap INTEGRATION_HOST=$ip bundle exec ruby test/integration/test_search.rb
+
+# run integration tests by default
+$ export INTEGRATION=openldap
+$ export INTEGRATION_HOST=$ip
+
+# now run tests without having to set ENV variables
+$ time bundle exec rake
+```
+
+You may need to `gem install vagrant` first in order to provision the VM.
diff --git a/test/support/vm/openldap/Vagrantfile b/test/support/vm/openldap/Vagrantfile
new file mode 100644
index 0000000..96233e9
--- /dev/null
+++ b/test/support/vm/openldap/Vagrantfile
@@ -0,0 +1,33 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
+VAGRANTFILE_API_VERSION = "2"
+
+Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
+  config.vm.hostname = "rubyldap.com"
+
+  config.vm.box = "hashicorp/precise64"
+
+  config.vm.network "private_network", type: :dhcp
+
+  config.ssh.forward_agent = true
+
+  config.vm.provision "shell", inline: "apt-get update; exec env /vagrant_data/script/install-openldap"
+
+  config.vm.synced_folder "../../../..", "/vagrant_data"
+
+  config.vm.provider "vmware_fusion" do |vb, override|
+    override.vm.box = "hashicorp/precise64"
+    vb.memory = 4596
+    vb.vmx["displayname"] = "integration tests vm"
+    vb.vmx["numvcpus"] = "2"
+  end
+
+  config.vm.provider "virtualbox" do |vb, override|
+    vb.memory = 4096
+    vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
+    vb.customize ["modifyvm", :id, "--chipset", "ich9"]
+    vb.customize ["modifyvm", :id, "--vram", "16"]
+  end
+end
diff --git a/test/test_dn.rb b/test/test_dn.rb
new file mode 100644
index 0000000..0cb2ec5
--- /dev/null
+++ b/test/test_dn.rb
@@ -0,0 +1,44 @@
+require_relative 'test_helper'
+require 'net/ldap/dn'
+
+class TestDN < Test::Unit::TestCase
+  def test_escape
+    assert_equal '\\,\\+\\"\\\\\\<\\>\\;', Net::LDAP::DN.escape(',+"\\<>;')
+  end
+
+  def test_escape_on_initialize
+    dn = Net::LDAP::DN.new('cn', ',+"\\<>;', 'ou=company')
+    assert_equal 'cn=\\,\\+\\"\\\\\\<\\>\\;,ou=company', dn.to_s
+  end
+
+  def test_to_a
+    dn = Net::LDAP::DN.new('cn=James, ou=Company\\,\\20LLC')
+    assert_equal ['cn','James','ou','Company, LLC'], dn.to_a
+  end
+
+  def test_to_a_parenthesis
+    dn = Net::LDAP::DN.new('cn =  \ James , ou  =  "Comp\28ny"  ')
+    assert_equal ['cn',' James','ou','Comp(ny'], dn.to_a
+  end
+
+  def test_to_a_hash_symbol
+    dn = Net::LDAP::DN.new('1.23.4=  #A3B4D5  ,ou=Company')
+    assert_equal ['1.23.4','#A3B4D5','ou','Company'], dn.to_a
+  end
+
+  # TODO: raise a more specific exception than RuntimeError
+  def test_bad_input_raises_error
+    [
+      'cn=James,',
+      'cn=#aa aa',
+      'cn="James',
+      'cn=J\ames',
+      'cn=\\',
+      '1.2.d=Value',
+      'd1.2=Value',
+    ].each do |input|
+      dn = Net::LDAP::DN.new(input)
+      assert_raises(RuntimeError) { dn.to_a }
+    end
+  end
+end
diff --git a/test/test_entry.rb b/test/test_entry.rb
index 73898d6..e218474 100644
--- a/test/test_entry.rb
+++ b/test/test_entry.rb
@@ -1,59 +1,65 @@
-require 'common'
+require_relative 'test_helper'
 
-=begin
 class TestEntry < Test::Unit::TestCase
-Commented out until I can make it a spec.
-  context "An instance of Entry" do
-    setup do
-      @entry = Net::LDAP::Entry.new 'cn=Barbara,o=corp'
-    end
-
-    should "be initialized with the DN" do
-      assert_equal 'cn=Barbara,o=corp', @entry.dn
-    end
-
-    should 'return an empty array when accessing a nonexistent attribute (index lookup)' do
-      assert_equal [], @entry['sn']
-    end
-
-    should 'return an empty array when accessing a nonexistent attribute (method call)' do
-      assert_equal [], @entry.sn
-    end
-
-    should 'create an attribute on assignment (index lookup)' do
-      @entry['sn'] = 'Jensen'
-      assert_equal ['Jensen'], @entry['sn']
-    end
-
-    should 'create an attribute on assignment (method call)' do
-      @entry.sn = 'Jensen'
-      assert_equal ['Jensen'], @entry.sn
-    end
-
-    should 'have attributes accessible by index lookup' do
-      @entry['sn'] = 'Jensen'
-      assert_equal ['Jensen'], @entry['sn']
-    end
-
-    should 'have attributes accessible using a Symbol as the index' do
-      @entry[:sn] = 'Jensen'
-      assert_equal ['Jensen'], @entry[:sn]
-    end
-
-    should 'have attributes accessible by method call' do
-      @entry['sn'] = 'Jensen'
-      assert_equal ['Jensen'], @entry.sn
-    end
-
-    should 'ignore case of attribute names' do
-      @entry['sn'] = 'Jensen'
-      assert_equal ['Jensen'], @entry.sn
-      assert_equal ['Jensen'], @entry.Sn
-      assert_equal ['Jensen'], @entry.SN
-      assert_equal ['Jensen'], @entry['sn']
-      assert_equal ['Jensen'], @entry['Sn']
-      assert_equal ['Jensen'], @entry['SN']
-    end
-	end
+  def setup
+    @entry = Net::LDAP::Entry.new 'cn=Barbara,o=corp'
+  end
+
+  def test_dn
+    assert_equal 'cn=Barbara,o=corp', @entry.dn
+  end
+
+  def test_empty_array_when_accessing_nonexistent_attribute
+    assert_equal [], @entry['sn']
+  end
+
+  def test_attribute_assignment
+    @entry['sn'] = 'Jensen'
+    assert_equal ['Jensen'], @entry['sn']
+    assert_equal ['Jensen'], @entry.sn
+    assert_equal ['Jensen'], @entry[:sn]
+
+    @entry[:sn] = 'Jensen'
+    assert_equal ['Jensen'], @entry['sn']
+    assert_equal ['Jensen'], @entry.sn
+    assert_equal ['Jensen'], @entry[:sn]
+
+    @entry.sn = 'Jensen'
+    assert_equal ['Jensen'], @entry['sn']
+    assert_equal ['Jensen'], @entry.sn
+    assert_equal ['Jensen'], @entry[:sn]
+  end
+
+  def test_case_insensitive_attribute_names
+    @entry['sn'] = 'Jensen'
+    assert_equal ['Jensen'], @entry.sn
+    assert_equal ['Jensen'], @entry.Sn
+    assert_equal ['Jensen'], @entry.SN
+    assert_equal ['Jensen'], @entry['sn']
+    assert_equal ['Jensen'], @entry['Sn']
+    assert_equal ['Jensen'], @entry['SN']
+  end
+end
+
+class TestEntryLDIF < Test::Unit::TestCase
+  def setup
+    @entry = Net::LDAP::Entry.from_single_ldif_string(
+      %Q{dn: something
+foo: foo
+barAttribute: bar
+      })
+  end
+
+  def test_attribute
+    assert_equal ['foo'], @entry.foo
+    assert_equal ['foo'], @entry.Foo
+  end
+
+  def test_modify_attribute
+    @entry.foo = 'bar'
+    assert_equal ['bar'], @entry.foo
+
+    @entry.fOo= 'baz'
+    assert_equal ['baz'], @entry.foo
+  end
 end
-=end
diff --git a/test/test_filter.rb b/test/test_filter.rb
index 03436e0..2bcccd9 100644
--- a/test/test_filter.rb
+++ b/test/test_filter.rb
@@ -1,4 +1,4 @@
-require 'common'
+require_relative 'test_helper'
 
 class TestFilter < Test::Unit::TestCase
   Filter = Net::LDAP::Filter
@@ -9,11 +9,11 @@ class TestFilter < Test::Unit::TestCase
   end
 
   def test_invalid_filter_string
-    assert_raises(Net::LDAP::LdapError) { Filter.from_rfc2254("") }
+    assert_raises(Net::LDAP::FilterSyntaxInvalidError) { Filter.from_rfc2254("") }
   end
 
   def test_invalid_filter
-    assert_raises(Net::LDAP::LdapError) {
+    assert_raises(Net::LDAP::OperatorError) {
       # This test exists to prove that our constructor blocks unknown filter
       # types. All filters must be constructed using helpers.
       Filter.__send__(:new, :xx, nil, nil)
@@ -28,7 +28,7 @@ class TestFilter < Test::Unit::TestCase
     assert_equal("(uid=\\2A)", Filter.equals("uid", "*").to_s)
     assert_equal("(uid=\\28*)", Filter.begins("uid", "(").to_s)
     assert_equal("(uid=*\\29)", Filter.ends("uid", ")").to_s)
-    assert_equal("(uid=*\\5C*)", Filter.contains("uid", "\\").to_s)	
+    assert_equal("(uid=*\\5C*)", Filter.contains("uid", "\\").to_s)
   end
 
   def test_c2
@@ -120,3 +120,104 @@ class TestFilter < Test::Unit::TestCase
     end
   end
 end
+
+# tests ported over from rspec. Not sure if these overlap with the above
+# https://github.com/ruby-ldap/ruby-net-ldap/pull/121
+class TestFilterRSpec < Test::Unit::TestCase
+  def test_ex_convert
+    assert_equal '(foo:=bar)', Net::LDAP::Filter.ex('foo', 'bar').to_s
+  end
+
+  def test_ex_rfc2254_roundtrip
+    filter = Net::LDAP::Filter.ex('foo', 'bar')
+    assert_equal filter, Net::LDAP::Filter.from_rfc2254(filter.to_s)
+  end
+
+  def test_ber_conversion
+    filter = Net::LDAP::Filter.ex('foo', 'bar')
+    ber = filter.to_ber
+    assert_equal filter, Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax))
+  end
+
+  [
+    '(o:dn:=Ace Industry)',
+    '(:dn:2.4.8.10:=Dino)',
+    '(cn:dn:1.2.3.4.5:=John Smith)',
+    '(sn:dn:2.4.6.8.10:=Barbara Jones)',
+    '(&(sn:dn:2.4.6.8.10:=Barbara Jones))'
+  ].each_with_index do |filter_str, index|
+    define_method "test_decode_filter_#{index}" do
+      filter = Net::LDAP::Filter.from_rfc2254(filter_str)
+      assert_kind_of Net::LDAP::Filter, filter
+    end
+
+    define_method "test_ber_conversion_#{index}" do
+      filter = Net::LDAP::Filter.from_rfc2254(filter_str)
+      ber = Net::LDAP::Filter.from_rfc2254(filter_str).to_ber
+      assert_equal filter, Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax))
+    end
+  end
+
+  def test_apostrophes
+    assert_equal "(uid=O'Keefe)", Net::LDAP::Filter.construct("uid=O'Keefe").to_rfc2254
+  end
+
+  def test_equals
+    assert_equal Net::LDAP::Filter.eq('dn', 'f\2Aoo'), Net::LDAP::Filter.equals('dn', 'f*oo')
+  end
+
+  def test_begins
+    assert_equal Net::LDAP::Filter.eq('dn', 'f\2Aoo*'), Net::LDAP::Filter.begins('dn', 'f*oo')
+  end
+
+  def test_ends
+    assert_equal Net::LDAP::Filter.eq('dn', '*f\2Aoo'), Net::LDAP::Filter.ends('dn', 'f*oo')
+  end
+
+  def test_contains
+    assert_equal Net::LDAP::Filter.eq('dn', '*f\2Aoo*'), Net::LDAP::Filter.contains('dn', 'f*oo')
+  end
+
+  def test_escape
+    # escapes nul, *, (, ) and \\
+    assert_equal "\\00\\2A\\28\\29\\5C", Net::LDAP::Filter.escape("\0*()\\")
+  end
+
+  def test_well_known_ber_string
+    ber = "\xa4\x2d" \
+      "\x04\x0b" "objectclass" \
+      "\x30\x1e" \
+      "\x80\x08" "foo" "*\\" "bar" \
+      "\x81\x08" "foo" "*\\" "bar" \
+      "\x82\x08" "foo" "*\\" "bar".b
+
+    [
+      "foo" "\\2A\\5C" "bar",
+      "foo" "\\2a\\5c" "bar",
+      "foo" "\\2A\\5c" "bar",
+      "foo" "\\2a\\5C" "bar"
+    ].each do |escaped|
+      # unescapes escaped characters
+      filter = Net::LDAP::Filter.eq("objectclass", "#{escaped}*#{escaped}*#{escaped}")
+      assert_equal ber, filter.to_ber
+    end
+  end
+
+  def test_parse_ber_escapes_characters
+    ber = "\xa4\x2d" \
+      "\x04\x0b" "objectclass" \
+      "\x30\x1e" \
+      "\x80\x08" "foo" "*\\" "bar" \
+      "\x81\x08" "foo" "*\\" "bar" \
+      "\x82\x08" "foo" "*\\" "bar".b
+
+    escaped = Net::LDAP::Filter.escape("foo" "*\\" "bar")
+    filter = Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax))
+    assert_equal "(objectclass=#{escaped}*#{escaped}*#{escaped})", filter.to_s
+  end
+
+  def test_unescape_fixnums
+    filter = Net::LDAP::Filter.eq("objectclass", 3)
+    assert_equal "\xA3\x10\x04\vobjectclass\x04\x013".b, filter.to_ber
+  end
+end
diff --git a/test/test_filter_parser.rb b/test/test_filter_parser.rb
new file mode 100644
index 0000000..210e021
--- /dev/null
+++ b/test/test_filter_parser.rb
@@ -0,0 +1,20 @@
+# encoding: utf-8
+require_relative 'test_helper'
+
+class TestFilterParser < Test::Unit::TestCase
+  def test_ascii
+    assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(cn=name)")
+  end
+
+  def test_multibyte_characters
+    assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(cn=名前)")
+  end
+
+  def test_brackets
+    assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(cn=[{something}])")
+  end
+
+  def test_colons
+    assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(ismemberof=cn=edu:berkeley:app:calmessages:deans,ou=campus groups,dc=berkeley,dc=edu)")
+  end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..640b0e2
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,66 @@
+# Add 'lib' to load path.
+require 'test/unit'
+require 'net/ldap'
+require 'flexmock/test_unit'
+
+# Whether integration tests should be run.
+INTEGRATION = ENV.fetch("INTEGRATION", "skip") != "skip"
+
+# The CA file to verify certs against for tests.
+# Override with CA_FILE env variable; otherwise checks for the VM-specific path
+# and falls back to the test/fixtures/cacert.pem for local testing.
+CA_FILE =
+  ENV.fetch("CA_FILE") do
+    if File.exist?("/etc/ssl/certs/cacert.pem")
+      "/etc/ssl/certs/cacert.pem"
+    else
+      File.expand_path("fixtures/cacert.pem", File.dirname(__FILE__))
+    end
+  end
+
+if RUBY_VERSION < "2.0"
+  class String
+    def b
+      self
+    end
+  end
+end
+
+class MockInstrumentationService
+  def initialize
+    @events = {}
+  end
+
+  def instrument(event, payload)
+    result = yield(payload)
+    @events[event] ||= []
+    @events[event] << [payload, result]
+    result
+  end
+
+  def subscribe(event)
+    @events[event] ||= []
+    @events[event]
+  end
+end
+
+class LDAPIntegrationTestCase < Test::Unit::TestCase
+  # If integration tests aren't enabled, noop these tests.
+  if !INTEGRATION
+    def run(*)
+      self
+    end
+  end
+
+  def setup
+    @service = MockInstrumentationService.new
+    @ldap = Net::LDAP.new \
+      host:           ENV.fetch('INTEGRATION_HOST', 'localhost'),
+      port:           389,
+      admin_user:     'uid=admin,dc=rubyldap,dc=com',
+      admin_password: 'passworD1',
+      search_domains: %w(dc=rubyldap,dc=com),
+      uid:            'uid',
+      instrumentation_service: @service
+  end
+end
diff --git a/test/test_ldap.rb b/test/test_ldap.rb
new file mode 100644
index 0000000..9704b34
--- /dev/null
+++ b/test/test_ldap.rb
@@ -0,0 +1,60 @@
+require 'test_helper'
+
+class TestLDAPInstrumentation < Test::Unit::TestCase
+  def setup
+    @connection = flexmock(:connection, :close => true)
+    flexmock(Net::LDAP::Connection).should_receive(:new).and_return(@connection)
+
+    @service = MockInstrumentationService.new
+    @subject = Net::LDAP.new \
+      :host => "test.mocked.com", :port => 636,
+      :force_no_page => true, # so server capabilities are not queried
+      :instrumentation_service => @service
+  end
+
+  def test_instrument_bind
+    events = @service.subscribe "bind.net_ldap"
+
+    bind_result = flexmock(:bind_result, :success? => true)
+    flexmock(@connection).should_receive(:bind).with(Hash).and_return(bind_result)
+
+    assert @subject.bind
+
+    payload, result = events.pop
+    assert result
+    assert_equal bind_result, payload[:bind]
+  end
+
+  def test_instrument_search
+    events = @service.subscribe "search.net_ldap"
+
+    flexmock(@connection).should_receive(:bind).and_return(flexmock(:bind_result, :result_code => Net::LDAP::ResultCodeSuccess))
+    flexmock(@connection).should_receive(:search).with(Hash, Proc).
+                yields(entry = Net::LDAP::Entry.new("uid=user1,ou=users,dc=example,dc=com")).
+                and_return(flexmock(:search_result, :success? => true, :result_code => Net::LDAP::ResultCodeSuccess))
+
+    refute_nil @subject.search(:filter => "(uid=user1)")
+
+    payload, result = events.pop
+    assert_equal [entry], result
+    assert_equal [entry], payload[:result]
+    assert_equal "(uid=user1)", payload[:filter]
+  end
+
+  def test_instrument_search_with_size
+    events = @service.subscribe "search.net_ldap"
+
+    flexmock(@connection).should_receive(:bind).and_return(flexmock(:bind_result, :result_code => Net::LDAP::ResultCodeSuccess))
+    flexmock(@connection).should_receive(:search).with(Hash, Proc).
+                yields(entry = Net::LDAP::Entry.new("uid=user1,ou=users,dc=example,dc=com")).
+                and_return(flexmock(:search_result, :success? => true, :result_code => Net::LDAP::ResultCodeSizeLimitExceeded))
+
+    refute_nil @subject.search(:filter => "(uid=user1)", :size => 1)
+
+    payload, result = events.pop
+    assert_equal [entry], result
+    assert_equal [entry], payload[:result]
+    assert_equal "(uid=user1)", payload[:filter]
+    assert_equal result.size, payload[:size]
+  end
+end
diff --git a/test/test_ldap_connection.rb b/test/test_ldap_connection.rb
index a364301..96b542a 100644
--- a/test/test_ldap_connection.rb
+++ b/test/test_ldap_connection.rb
@@ -1,6 +1,27 @@
-require 'common'
+require_relative 'test_helper'
+
+class TestLDAPConnection < Test::Unit::TestCase
+  def test_unresponsive_host
+    assert_raise Net::LDAP::Error do
+      Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
+    end
+  end
+
+  def test_blocked_port
+    flexmock(TCPSocket).should_receive(:new).and_raise(SocketError)
+    assert_raise Net::LDAP::Error do
+      Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
+    end
+  end
+
+  def test_raises_unknown_exceptions
+    error = Class.new(StandardError)
+    flexmock(TCPSocket).should_receive(:new).and_raise(error)
+    assert_raise error do
+      Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
+    end
+  end
 
-class TestLDAP < Test::Unit::TestCase
   def test_modify_ops_delete
     args = { :operations => [ [ :delete, "mail" ] ] }
     result = Net::LDAP::Connection.modify_ops(args[:operations])
@@ -21,4 +42,363 @@ class TestLDAP < Test::Unit::TestCase
     expected = [ "0#\n\x01\x020\x1E\x04\x04mail1\x16\x04\x14testuser at example.com" ]
     assert_equal(expected, result)
   end
+
+  def test_write
+    mock = flexmock("socket")
+    mock.should_receive(:write).with([1.to_ber, "request"].to_ber_sequence).and_return(true)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+    conn.send(:write, "request")
+  end
+
+  def test_write_with_controls
+    mock = flexmock("socket")
+    mock.should_receive(:write).with([1.to_ber, "request", "controls"].to_ber_sequence).and_return(true)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+    conn.send(:write, "request", "controls")
+  end
+
+  def test_write_increments_msgid
+    mock = flexmock("socket")
+    mock.should_receive(:write).with([1.to_ber, "request1"].to_ber_sequence).and_return(true)
+    mock.should_receive(:write).with([2.to_ber, "request2"].to_ber_sequence).and_return(true)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+    conn.send(:write, "request1")
+    conn.send(:write, "request2")
+  end
+end
+
+class TestLDAPConnectionSocketReads < Test::Unit::TestCase
+  def make_message(message_id, options = {})
+    options = {
+      app_tag: Net::LDAP::PDU::SearchResult,
+      code: Net::LDAP::ResultCodeSuccess,
+      matched_dn: "",
+      error_message: ""
+    }.merge(options)
+    result = Net::BER::BerIdentifiedArray.new([options[:code], options[:matched_dn], options[:error_message]])
+    result.ber_identifier = options[:app_tag]
+    [message_id, result]
+  end
+
+  def test_queued_read_drains_queue_before_read
+    result1a = make_message(1, error_message: "one")
+    result1b = make_message(1, error_message: "two")
+
+    mock = flexmock("socket")
+    mock.should_receive(:read_ber).and_return(result1b)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+
+    conn.message_queue[1].push Net::LDAP::PDU.new(result1a)
+
+    assert msg1 = conn.queued_read(1)
+    assert msg2 = conn.queued_read(1)
+
+    assert_equal 1, msg1.message_id
+    assert_equal "one", msg1.error_message
+    assert_equal 1, msg2.message_id
+    assert_equal "two", msg2.error_message
+  end
+
+  def test_queued_read_reads_until_message_id_match
+    result1 = make_message(1)
+    result2 = make_message(2)
+
+    mock = flexmock("socket")
+    mock.should_receive(:read_ber).
+      and_return(result1).
+      and_return(result2)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+
+    assert result = conn.queued_read(2)
+    assert_equal 2, result.message_id
+    assert_equal 1, conn.queued_read(1).message_id
+  end
+
+  def test_queued_read_modify
+    result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult)
+    result2 = make_message(2, app_tag: Net::LDAP::PDU::ModifyResponse)
+
+    mock = flexmock("socket")
+    mock.should_receive(:read_ber).
+      and_return(result1).
+      and_return(result2)
+    mock.should_receive(:write)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+
+    conn.next_msgid # simulates ongoing query
+
+    conn.instance_variable_get("@msgid")
+
+    assert result = conn.modify(dn: "uid=modified-user1,ou=People,dc=rubyldap,dc=com",
+                                operations: [[:add, :mail, "modified-user1 at example.com"]])
+    assert result.success?
+    assert_equal 2, result.message_id
+  end
+
+  def test_queued_read_add
+    result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult)
+    result2 = make_message(2, app_tag: Net::LDAP::PDU::AddResponse)
+
+    mock = flexmock("socket")
+    mock.should_receive(:read_ber).
+      and_return(result1).
+      and_return(result2)
+    mock.should_receive(:write)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+
+    conn.next_msgid # simulates ongoing query
+
+    assert result = conn.add(dn: "uid=added-user1,ou=People,dc=rubyldap,dc=com")
+    assert result.success?
+    assert_equal 2, result.message_id
+  end
+
+  def test_queued_read_rename
+    result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult)
+    result2 = make_message(2, app_tag: Net::LDAP::PDU::ModifyRDNResponse)
+
+    mock = flexmock("socket")
+    mock.should_receive(:read_ber).
+      and_return(result1).
+      and_return(result2)
+    mock.should_receive(:write)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+
+    conn.next_msgid # simulates ongoing query
+
+    assert result = conn.rename(
+      olddn:  "uid=renamable-user1,ou=People,dc=rubyldap,dc=com",
+      newrdn: "uid=renamed-user1"
+    )
+    assert result.success?
+    assert_equal 2, result.message_id
+  end
+
+  def test_queued_read_delete
+    result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult)
+    result2 = make_message(2, app_tag: Net::LDAP::PDU::DeleteResponse)
+
+    mock = flexmock("socket")
+    mock.should_receive(:read_ber).
+      and_return(result1).
+      and_return(result2)
+    mock.should_receive(:write)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+
+    conn.next_msgid # simulates ongoing query
+
+    assert result = conn.delete(dn: "uid=deletable-user1,ou=People,dc=rubyldap,dc=com")
+    assert result.success?
+    assert_equal 2, result.message_id
+  end
+
+  def test_queued_read_setup_encryption_with_start_tls
+    result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult)
+    result2 = make_message(2, app_tag: Net::LDAP::PDU::ExtendedResponse)
+
+    mock = flexmock("socket")
+    mock.should_receive(:read_ber).
+      and_return(result1).
+      and_return(result2)
+    mock.should_receive(:write)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+    flexmock(Net::LDAP::Connection).should_receive(:wrap_with_ssl).with(mock, {}).
+      and_return(mock)
+
+    conn.next_msgid # simulates ongoing query
+
+    assert result = conn.setup_encryption(method: :start_tls)
+    assert_equal mock, result
+  end
+
+  def test_queued_read_bind_simple
+    result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult)
+    result2 = make_message(2, app_tag: Net::LDAP::PDU::BindResult)
+
+    mock = flexmock("socket")
+    mock.should_receive(:read_ber).
+      and_return(result1).
+      and_return(result2)
+    mock.should_receive(:write)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+
+    conn.next_msgid # simulates ongoing query
+
+    assert result = conn.bind(
+      method: :simple,
+      username: "uid=user1,ou=People,dc=rubyldap,dc=com",
+      password: "passworD1")
+    assert result.success?
+    assert_equal 2, result.message_id
+  end
+
+  def test_queued_read_bind_sasl
+    result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult)
+    result2 = make_message(2, app_tag: Net::LDAP::PDU::BindResult)
+
+    mock = flexmock("socket")
+    mock.should_receive(:read_ber).
+      and_return(result1).
+      and_return(result2)
+    mock.should_receive(:write)
+    conn = Net::LDAP::Connection.new(:socket => mock)
+
+    conn.next_msgid # simulates ongoing query
+
+    assert result = conn.bind(
+      method: :sasl,
+      mechanism: "fake",
+      initial_credential: "passworD1",
+      challenge_response: flexmock("challenge proc"))
+    assert result.success?
+    assert_equal 2, result.message_id
+  end
+end
+
+class TestLDAPConnectionErrors < Test::Unit::TestCase
+  def setup
+    @tcp_socket = flexmock(:connection)
+    @tcp_socket.should_receive(:write)
+    flexmock(TCPSocket).should_receive(:new).and_return(@tcp_socket)
+    @connection = Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
+  end
+
+  def test_error_failed_operation
+    ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeUnwillingToPerform, "", "The provided password value was rejected by a password validator:  The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.  The minimum number of characters from that set that must be present in user passwords is 1"])
+    ber.ber_identifier = Net::LDAP::PDU::ModifyResponse
+    @tcp_socket.should_receive(:read_ber).and_return([1, ber])
+
+    result = @connection.modify(:dn => "1", :operations => [[:replace, "mail", "something at sothsdkf.com"]])
+    assert result.failure?, "should be failure"
+    assert_equal "The provided password value was rejected by a password validator:  The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.  The minimum number of characters from that set that must be present in user passwords is 1", result.error_message
+  end
+
+  def test_no_error_on_success
+    ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""])
+    ber.ber_identifier = Net::LDAP::PDU::ModifyResponse
+    @tcp_socket.should_receive(:read_ber).and_return([1, ber])
+
+    result = @connection.modify(:dn => "1", :operations => [[:replace, "mail", "something at sothsdkf.com"]])
+    assert result.success?, "should be success"
+    assert_equal "", result.error_message
+  end
+end
+
+class TestLDAPConnectionInstrumentation < Test::Unit::TestCase
+  def setup
+    @tcp_socket = flexmock(:connection)
+    @tcp_socket.should_receive(:write)
+    flexmock(TCPSocket).should_receive(:new).and_return(@tcp_socket)
+
+    @service = MockInstrumentationService.new
+    @connection = Net::LDAP::Connection.new \
+      :host => 'test.mocked.com',
+      :port => 636,
+      :instrumentation_service => @service
+  end
+
+  def test_write_net_ldap_connection_event
+    ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""])
+    ber.ber_identifier = Net::LDAP::PDU::BindResult
+    read_result = [1, ber]
+    @tcp_socket.should_receive(:read_ber).and_return(read_result)
+
+    events = @service.subscribe "write.net_ldap_connection"
+
+    result = @connection.bind(method: :anon)
+    assert result.success?, "should be success"
+
+    # a write event
+    payload, result = events.pop
+    assert payload.has_key?(:result)
+    assert payload.has_key?(:content_length)
+  end
+
+  def test_read_net_ldap_connection_event
+    ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""])
+    ber.ber_identifier = Net::LDAP::PDU::BindResult
+    read_result = [1, ber]
+    @tcp_socket.should_receive(:read_ber).and_return(read_result)
+
+    events = @service.subscribe "read.net_ldap_connection"
+
+    result = @connection.bind(method: :anon)
+    assert result.success?, "should be success"
+
+    # a read event
+    payload, result = events.pop
+    assert payload.has_key?(:result)
+    assert_equal read_result, result
+  end
+
+  def test_parse_pdu_net_ldap_connection_event
+    ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""])
+    ber.ber_identifier = Net::LDAP::PDU::BindResult
+    read_result = [1, ber]
+    @tcp_socket.should_receive(:read_ber).and_return(read_result)
+
+    events = @service.subscribe "parse_pdu.net_ldap_connection"
+
+    result = @connection.bind(method: :anon)
+    assert result.success?, "should be success"
+
+    # a parse_pdu event
+    payload, result = events.pop
+    assert payload.has_key?(:pdu)
+    assert payload.has_key?(:app_tag)
+    assert payload.has_key?(:message_id)
+    assert_equal Net::LDAP::PDU::BindResult, payload[:app_tag]
+    assert_equal 1, payload[:message_id]
+    pdu = payload[:pdu]
+    assert_equal Net::LDAP::ResultCodeSuccess, pdu.result_code
+  end
+
+  def test_bind_net_ldap_connection_event
+    ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""])
+    ber.ber_identifier = Net::LDAP::PDU::BindResult
+    bind_result = [1, ber]
+    @tcp_socket.should_receive(:read_ber).and_return(bind_result)
+
+    events = @service.subscribe "bind.net_ldap_connection"
+
+    result = @connection.bind(method: :anon)
+    assert result.success?, "should be success"
+
+    # a read event
+    payload, result = events.pop
+    assert payload.has_key?(:result)
+    assert result.success?, "should be success"
+  end
+
+  def test_search_net_ldap_connection_event
+    # search data
+    search_data_ber = Net::BER::BerIdentifiedArray.new([1, [
+      "uid=user1,ou=People,dc=rubyldap,dc=com",
+      [ ["uid", ["user1"]] ]
+    ]])
+    search_data_ber.ber_identifier = Net::LDAP::PDU::SearchReturnedData
+    search_data = [1, search_data_ber]
+    # search result (end of results)
+    search_result_ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""])
+    search_result_ber.ber_identifier = Net::LDAP::PDU::SearchResult
+    search_result = [1, search_result_ber]
+    @tcp_socket.should_receive(:read_ber).and_return(search_data).
+                                          and_return(search_result)
+
+    events = @service.subscribe "search.net_ldap_connection"
+    unread = @service.subscribe "search_messages_unread.net_ldap_connection"
+
+    result = @connection.search(filter: "(uid=user1)", base: "ou=People,dc=rubyldap,dc=com")
+    assert result.success?, "should be success"
+
+    # a search event
+    payload, result = events.pop
+    assert payload.has_key?(:result)
+    assert payload.has_key?(:filter)
+    assert_equal "(uid=user1)", payload[:filter].to_s
+    assert result
+
+    # ensure no unread
+    assert unread.empty?, "should not have any leftover unread messages"
+  end
 end
diff --git a/test/test_ldif.rb b/test/test_ldif.rb
index fb4d5ee..988c315 100644
--- a/test/test_ldif.rb
+++ b/test/test_ldif.rb
@@ -1,6 +1,6 @@
 # $Id: testldif.rb 61 2006-04-18 20:55:55Z blackhedd $
 
-require 'common'
+require_relative 'test_helper'
 
 require 'digest/sha1'
 require 'base64'
@@ -13,6 +13,12 @@ class TestLdif < Test::Unit::TestCase
     assert_equal(true, ds.empty?)
   end
 
+  def test_ldif_with_version
+    io = StringIO.new("version: 1")
+    ds = Net::LDAP::Dataset.read_ldif(io)
+    assert_equal "1", ds.version
+  end
+
   def test_ldif_with_comments
     str = ["# Hello from LDIF-land", "# This is an unterminated comment"]
     io = StringIO.new(str[0] + "\r\n" + str[1])
@@ -47,6 +53,18 @@ class TestLdif < Test::Unit::TestCase
     assert_equal(true, ds.has_key?("key"))
   end
 
+  def test_ldif_with_base64_dn
+    str = "dn:: Q049QmFzZTY0IGRuIHRlc3QsT1U9VGVzdCxPVT1Vbml0cyxEQz1leGFtcGxlLERDPWNvbQ==\r\n\r\n"
+    ds = Net::LDAP::Dataset::read_ldif(StringIO.new(str))
+    assert_equal(true, ds.has_key?("CN=Base64 dn test,OU=Test,OU=Units,DC=example,DC=com"))
+  end
+
+  def test_ldif_with_base64_dn_and_continuation_lines
+    str = "dn:: Q049QmFzZTY0IGRuIHRlc3Qgd2l0aCBjb250aW51YXRpb24gbGluZSxPVT1UZXN0LE9VPVVua\r\n XRzLERDPWV4YW1wbGUsREM9Y29t\r\n\r\n"
+    ds = Net::LDAP::Dataset::read_ldif(StringIO.new(str))
+    assert_equal(true, ds.has_key?("CN=Base64 dn test with continuation line,OU=Test,OU=Units,DC=example,DC=com"))
+  end
+
   # TODO, INADEQUATE. We need some more tests
   # to verify the content.
   def test_ldif
@@ -76,4 +94,11 @@ class TestLdif < Test::Unit::TestCase
     assert_equal(entries.size, ds.size)
     assert_equal(entries.sort, ds.to_ldif.grep(/^dn:\s*/) { $'.chomp })
   end
+
+  def test_to_ldif_with_version
+    ds = Net::LDAP::Dataset.new
+    ds.version = "1"
+
+    assert_equal "version: 1", ds.to_ldif_string.chomp
+  end
 end
diff --git a/test/test_password.rb b/test/test_password.rb
index abc8c22..87b47d9 100644
--- a/test/test_password.rb
+++ b/test/test_password.rb
@@ -1,17 +1,10 @@
 # $Id: testpsw.rb 72 2006-04-24 21:58:14Z blackhedd $
 
-require 'common'
+require_relative 'test_helper'
 
 class TestPassword < Test::Unit::TestCase
-
   def test_psw
-    assert_equal(
-	"{MD5}xq8jwrcfibi0sZdZYNkSng==",
-	Net::LDAP::Password.generate( :md5, "cashflow" ))
-
-    assert_equal(
-	"{SHA}YE4eGkN4BvwNN1f5R7CZz0kFn14=",
-	Net::LDAP::Password.generate( :sha, "cashflow" ))
+    assert_equal("{MD5}xq8jwrcfibi0sZdZYNkSng==", Net::LDAP::Password.generate( :md5, "cashflow" ))
+    assert_equal("{SHA}YE4eGkN4BvwNN1f5R7CZz0kFn14=", Net::LDAP::Password.generate( :sha, "cashflow" ))
   end
-
 end
diff --git a/test/test_rename.rb b/test/test_rename.rb
index db82340..6e6ee65 100644
--- a/test/test_rename.rb
+++ b/test/test_rename.rb
@@ -1,7 +1,7 @@
-require 'common'
+require_relative 'test_helper'
 
 # Commented out since it assumes you have a live LDAP server somewhere. This
-# will be migrated to the integration specs, as soon as they are ready. 
+# will be migrated to the integration specs, as soon as they are ready.
 =begin
 class TestRename < Test::Unit::TestCase
   HOST= '10.10.10.71'
diff --git a/test/test_search.rb b/test/test_search.rb
new file mode 100644
index 0000000..e349d0b
--- /dev/null
+++ b/test/test_search.rb
@@ -0,0 +1,39 @@
+# -*- ruby encoding: utf-8 -*-
+require_relative 'test_helper'
+
+class TestSearch < Test::Unit::TestCase
+  class FakeConnection
+    def search(args)
+      OpenStruct.new(:result_code => Net::LDAP::ResultCodeOperationsError, :message => "error", :success? => false)
+    end
+  end
+
+  def setup
+    @service = MockInstrumentationService.new
+    @connection = Net::LDAP.new :instrumentation_service => @service
+    @connection.instance_variable_set(:@open_connection, FakeConnection.new)
+  end
+
+  def test_true_result
+    assert_nil @connection.search(:return_result => true)
+  end
+
+  def test_false_result
+    refute @connection.search(:return_result => false)
+  end
+
+  def test_no_result
+    assert_nil @connection.search
+  end
+
+  def test_instrumentation_publishes_event
+    events = @service.subscribe "search.net_ldap"
+
+    @connection.search(:filter => "test")
+
+    payload, result = events.pop
+    assert payload.has_key?(:result)
+    assert payload.has_key?(:filter)
+    assert_equal "test", payload[:filter]
+  end
+end
diff --git a/test/test_snmp.rb b/test/test_snmp.rb
index 065025e..fe1ee16 100644
--- a/test/test_snmp.rb
+++ b/test/test_snmp.rb
@@ -1,6 +1,6 @@
 # $Id: testsnmp.rb 231 2006-12-21 15:09:29Z blackhedd $
 
-require 'common'
+require_relative 'test_helper'
 require 'net/snmp'
 
 class TestSnmp < Test::Unit::TestCase
diff --git a/test/test_ssl_ber.rb b/test/test_ssl_ber.rb
new file mode 100644
index 0000000..7711558
--- /dev/null
+++ b/test/test_ssl_ber.rb
@@ -0,0 +1,40 @@
+require_relative 'test_helper'
+require 'timeout'
+
+class TestSSLBER < Test::Unit::TestCase
+  # Transmits str to @to and reads it back from @from.
+  #
+  def transmit(str)
+    Timeout::timeout(1) do
+      @to.write(str)
+      @to.close
+
+      @from.read
+    end
+  end
+
+  def setup
+    @from, @to = IO.pipe
+
+    # The production code operates on sockets, which do need #connect called
+    # on them to work. Pipes are more robust for this test, so we'll skip
+    # the #connect call since it fails.
+    #
+    # TODO: Replace test with real socket
+    # https://github.com/ruby-ldap/ruby-net-ldap/pull/121#discussion_r18746386
+    flexmock(OpenSSL::SSL::SSLSocket).
+      new_instances.should_receive(:connect => nil)
+
+    @to   = Net::LDAP::Connection.wrap_with_ssl(@to)
+    @from = Net::LDAP::Connection.wrap_with_ssl(@from)
+  end
+
+  def test_transmit_strings
+    assert_equal "foo", transmit("foo")
+  end
+
+  def test_transmit_ber_encoded_numbers
+    @to.write 1234.to_ber
+    assert_equal 1234, @from.read_ber
+  end
+end

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-net-ldap.git



More information about the Pkg-ruby-extras-commits mailing list