ruby-ldapserver 0.5.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,220 @@
1
+ require 'ldap/server/dn'
2
+ require 'ldap/server/util'
3
+ require 'ldap/server/trie'
4
+ require 'ldap/server/request'
5
+ require 'ldap/server/filter'
6
+
7
+ module LDAP
8
+ class Server
9
+ class Router
10
+ @logger
11
+ @routes
12
+
13
+ # Scope
14
+ BaseObject = 0
15
+ SingleLevel = 1
16
+ WholeSubtree = 2
17
+
18
+ # DerefAliases
19
+ NeverDerefAliases = 0
20
+ DerefInSearching = 1
21
+ DerefFindingBaseObj = 2
22
+ DerefAlways = 3
23
+
24
+ def initialize(logger, &block)
25
+ @logger = logger
26
+
27
+ @routes = Hash.new
28
+ @routes = Trie.new do |trie|
29
+ # Add an artificial LDAP component
30
+ trie << "op=bind"
31
+ trie << "op=search"
32
+ end
33
+
34
+ self.instance_eval(&block)
35
+ end
36
+
37
+ def log_exception(e, level = :error)
38
+ @logger.send level, e.message
39
+ e.backtrace.each { |line| @logger.send level, "\tfrom#{line}" } if e.backtrace
40
+ end
41
+
42
+ ######################
43
+ ### Initialization ###
44
+ ######################
45
+
46
+ def route(operation, hash)
47
+ hash.each do |key, value|
48
+ if key.nil?
49
+ @routes.insert "op=#{operation.to_s}", value
50
+ @logger.debug "map operation #{operation.to_s} all routes to #{value}"
51
+ else
52
+ @routes.insert "#{key},op=#{operation.to_s}", value
53
+ @logger.debug "map #{operation.to_s} #{key} to #{value}"
54
+ end
55
+ end
56
+ end
57
+
58
+ def method_missing(name, *args, &block)
59
+ if [:bind, :search, :add, :modify, :modifydn, :del, :compare].include? name
60
+ send :route, name, *args
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+
67
+ ####################################################
68
+ ### Methods to parse and route each request type ###
69
+ ####################################################
70
+ def parse_route(dn, method)
71
+ route, action = @routes.match("#{dn},op=#{method.to_s}")
72
+ if not route or route.empty?
73
+ @logger.warn "No route defined for \'#{route}\'"
74
+ raise LDAP::ResultError::UnwillingToPerform
75
+ end
76
+ if action.nil?
77
+ @logger.error "No action defined for route \'#{route}\'"
78
+ raise LDAP::ResultError::UnwillingToPerform
79
+ end
80
+
81
+ class_name = action.split('#').first
82
+ method_name = action.split('#').last
83
+
84
+ params = LDAP::Server::DN.new("#{dn},op=#{method.to_s}").parse(route)
85
+
86
+ return class_name, method_name, params
87
+ end
88
+
89
+ def do_bind(connection, messageId, protocolOp, controls) # :nodoc:
90
+ request = Request.new(connection, messageId)
91
+ version = protocolOp.value[0].value
92
+ dn = protocolOp.value[1].value
93
+ dn = nil if dn.empty?
94
+ authentication = protocolOp.value[2]
95
+
96
+ @logger.debug "subject:#{connection.binddn} predicate:bind object:#{dn}"
97
+
98
+ # Find a route in the routing tree
99
+ class_name, method_name, params = parse_route(dn, :bind)
100
+
101
+ case authentication.tag # tag_class == :CONTEXT_SPECIFIC (check why)
102
+ when 0
103
+ Object.const_get(class_name).send method_name, request, version, dn, authentication.value, params
104
+ when 3
105
+ mechanism = authentication.value[0].value
106
+ credentials = authentication.value[1].value
107
+ # sasl_bind(version, dn, mechanism, credentials)
108
+ # FIXME: needs to exchange further BindRequests
109
+ # route_sasl_bind(request, version, dn, mechanism, credentials)
110
+ raise LDAP::ResultError::AuthMethodNotSupported
111
+ else
112
+ raise LDAP::ResultError::ProtocolError, "BindRequest bad AuthenticationChoice"
113
+ end
114
+ request.send_BindResponse(0)
115
+ return dn, version
116
+ rescue NoMethodError => e
117
+ log_exception e
118
+ request.send_BindResponse(LDAP::ResultError::OperationsError.new.to_i, :errorMessage => e.message)
119
+ return nil, version
120
+ rescue LDAP::ResultError => e
121
+ log_exception e
122
+ request.send_BindResponse(e.to_i, :errorMessage => e.message)
123
+ return nil, version
124
+ end
125
+
126
+ def do_search(connection, messageId, protocolOp, controls) # :nodoc:
127
+ request = Request.new(connection, messageId)
128
+ server = connection.opt[:server]
129
+ schema = connection.opt[:schema]
130
+ baseObject = protocolOp.value[0].value
131
+ scope = protocolOp.value[1].value
132
+ deref = protocolOp.value[2].value
133
+ client_sizelimit = protocolOp.value[3].value
134
+ client_timelimit = protocolOp.value[4].value.to_i
135
+ request.typesOnly = protocolOp.value[5].value
136
+ filter = LDAP::Server::Filter::parse(protocolOp.value[6], schema)
137
+ request.attributes = protocolOp.value[7].value.collect {|x| x.value}
138
+
139
+ sizelimit = request.server_sizelimit
140
+ sizelimit = client_sizelimit if client_sizelimit > 0 and
141
+ (sizelimit.nil? or client_sizelimit < sizelimit)
142
+ request.sizelimit = sizelimit
143
+
144
+ if baseObject.empty? and scope == BaseObject
145
+ request.send_SearchResultEntry("", server.root_dse) if
146
+ server.root_dse and LDAP::Server::Filter.run(filter, server.root_dse)
147
+ request.send_SearchResultDone(0)
148
+ return
149
+ elsif schema and baseObject == schema.subschema_dn
150
+ request.send_SearchResultEntry(baseObject, schema.subschema_subentry) if
151
+ schema and schema.subschema_subentry and
152
+ LDAP::Server::Filter.run(filter, schema.subschema_subentry)
153
+ request.send_SearchResultDone(0)
154
+ return
155
+ end
156
+
157
+ t = request.server_timelimit || 10
158
+ t = client_timelimit if client_timelimit > 0 and client_timelimit < t
159
+
160
+ @logger.debug "subject:#{connection.binddn} predicate:search object:#{baseObject}"
161
+
162
+ # Find a route in the routing tree
163
+ class_name, method_name, params = parse_route(baseObject, :search)
164
+
165
+ Timeout::timeout(t, LDAP::ResultError::TimeLimitExceeded) do
166
+ Object.const_get(class_name).send method_name, request, baseObject, scope, deref, filter, params
167
+ end
168
+ request.send_SearchResultDone(0)
169
+
170
+ # Note that TimeLimitExceeded is a subclass of LDAP::ResultError
171
+ rescue LDAP::ResultError => e
172
+ request.send_SearchResultDone(e.to_i, :errorMessage=>e.message)
173
+
174
+ rescue Abandon
175
+ # send no response
176
+
177
+ # Since this Operation is running in its own thread, we have to
178
+ # catch all other exceptions. Otherwise, in the event of a programming
179
+ # error, this thread will silently terminate and the client will wait
180
+ # forever for a response.
181
+
182
+ rescue Exception => e
183
+ log_exception e
184
+ request.send_SearchResultDone(LDAP::ResultError::OperationsError.new.to_i, :errorMessage=>e.message)
185
+ end
186
+
187
+
188
+ ###########################################################
189
+ ### Methods to actually perform the work requested ###
190
+ ### Use the signatures below to write your own handlers ###
191
+ ###########################################################
192
+
193
+ # Handle a simple bind request; raise an exception if the bind is
194
+ # not acceptable, otherwise just return to accept the bind.
195
+ #
196
+ # Write your own class method using this signature
197
+
198
+ # def simple_bind(request, version, dn, password, params)
199
+ # if version != 3
200
+ # raise LDAP::ResultError::ProtocolError, "version 3 only"
201
+ # end
202
+ # if dn
203
+ # raise LDAP::ResultError::InappropriateAuthentication, "This server only supports anonymous bind"
204
+ # end
205
+ # end
206
+
207
+ # Handle a search request
208
+ #
209
+ # Call request. send_SearchResultEntry for each result found. Raise
210
+ # an exception if there is a problem. timeLimit, sizeLimit and
211
+ # typesOnly are taken care of, but you need to perform all
212
+ # authorisation checks yourself, using @connection.binddn
213
+
214
+ # def search(basedn, scope, deref, filter)
215
+ # debug "search(#{basedn}, #{scope}, #{deref}, #{filter})"
216
+ # raise LDAP::ResultError::UnwillingToPerform, "search not implemented"
217
+ # end
218
+ end
219
+ end
220
+ end
@@ -15,20 +15,31 @@ class Server
15
15
 
16
16
  # Create a new server. Options include all those to tcpserver/preforkserver
17
17
  # plus:
18
- # :operation_class=>Class - set Operation handler class
19
- # :operation_args=>[...] - args to Operation.new
20
- # :ssl_key_file=>pem, :ssl_cert_file=>pem - enable SSL
21
- # :ssl_ca_path=>directory - verify peer certificates
22
- # :schema=>Schema - Schema object
23
- # :namingContexts=>[dn, ...] - base DN(s) we answer
24
-
18
+ # either
19
+ # :router=>Router - request router instance
20
+ # or
21
+ # :operation_class=>Class - set Operation handler class
22
+ # :operation_args=>[...] - args to Operation.new
23
+ #
24
+ # :ssl_key_file=>pem, :ssl_cert_file=>pem - enable SSL
25
+ # :ssl_ca_path=>directory - verify peer certificates
26
+ # :schema=>Schema - Schema object
27
+ # :namingContexts=>[dn, ...] - base DN(s) we answer
28
+ #
29
+ # Specifying a :router always overrides :operation_class
30
+
25
31
  attr_reader :logger
26
32
 
27
33
  def initialize(opt = DEFAULT_OPT)
28
34
  @opt = opt
29
35
  @opt[:server] = self
30
- @opt[:operation_class] ||= LDAP::Server::Operation
31
- @opt[:operation_args] ||= []
36
+ if @opt[:router]
37
+ @opt.delete(:operation_class)
38
+ @opt.delete(:operation_args)
39
+ else
40
+ @opt[:operation_class] ||= LDAP::Server::Operation
41
+ @opt[:operation_args] ||= []
42
+ end
32
43
  unless @opt[:logger]
33
44
  @opt[:logger] ||= Logger.new($stderr)
34
45
  @opt[:logger].level = Logger::INFO
@@ -92,7 +103,11 @@ class Server
92
103
  end
93
104
 
94
105
  def join
95
- @thread.join
106
+ begin
107
+ @thread.join
108
+ rescue Interrupt
109
+ @logger.info "Exiting..."
110
+ end
96
111
  end
97
112
 
98
113
  def stop
@@ -230,6 +230,6 @@ class Server
230
230
  add("1.3.6.1.4.1.1466.115.121.1.40", "Octet String")
231
231
  add("1.3.6.1.4.1.1466.115.121.1.58", "Substring Assertion")
232
232
  end
233
-
233
+
234
234
  end # class Server
235
235
  end # module LDAP
@@ -1,4 +1,5 @@
1
1
  require 'socket'
2
+ require 'fileutils'
2
3
 
3
4
  module LDAP
4
5
  class Server
@@ -22,13 +23,25 @@ class Server
22
23
  # :nodelay=>true - set TCP_NODELAY option
23
24
 
24
25
  def self.tcpserver(opt, &blk)
25
- server = TCPServer.new(opt[:bindaddr] || "0.0.0.0", opt[:port])
26
+ if opt[:socket]
27
+ server = UNIXServer.new(opt[:socket])
28
+ FileUtils.chmod(0777, opt[:socket])
29
+ else
30
+ server = TCPServer.new(opt[:bindaddr] || "0.0.0.0", opt[:port])
31
+ end
26
32
 
27
33
  # Drop privileges if requested
28
34
  require 'etc' if opt[:group] or opt[:user]
29
35
  Process.gid = Process.egid = Etc.getgrnam(opt[:group]).gid if opt[:group]
30
36
  Process.uid = Process.euid = Etc.getpwnam(opt[:user]).uid if opt[:user]
31
-
37
+
38
+ Process.gid = opt[:gid] if opt[:gid]
39
+ Process.uid = opt[:uid] if opt[:uid]
40
+
41
+ if opt[:socket]
42
+ FileUtils.chown((opt[:user] || opt[:uid]), (opt[:group] || opt[:gid]), opt[:socket])
43
+ end
44
+
32
45
  # Typically the O/S will buffer response data for 100ms before sending.
33
46
  # If the response is sent as a single write() then there's no need for it.
34
47
  if opt[:nodelay]
@@ -86,4 +99,4 @@ if __FILE__ == $0
86
99
  end
87
100
  #sleep 10; t.raise Interrupt # uncomment to run for fixed time period
88
101
  t.join
89
- end
102
+ end
@@ -0,0 +1,92 @@
1
+ require 'ldap/server/dn'
2
+
3
+ module LDAP
4
+ class Server
5
+ class Trie
6
+
7
+ # Trie or prefix tree suitable for storing LDAP paths
8
+ # Variables (wildcards) are supported
9
+
10
+ class NodeNotFoundError < Error; end
11
+
12
+ attr_accessor :parent, :value, :children
13
+
14
+ # Create a new Trie. Use with a block
15
+ def initialize(parent = nil, value = nil)
16
+ @parent = parent
17
+ @value = value
18
+ @children = Hash.new
19
+
20
+ yield self if block_given?
21
+ end
22
+
23
+ # Insert a path (empty node)
24
+ def <<(dn)
25
+ insert(dn)
26
+ end
27
+
28
+ # Insert a node with a value
29
+ def insert(dn, value = nil)
30
+ dn = LDAP::Server::DN.new(dn || '') if not dn.is_a? LDAP::Server::DN
31
+ dn.reverse_each do |component|
32
+ @children[component] = Trie.new(self) if @children[component].nil?
33
+ dn.dname.pop
34
+ if dn.any?
35
+ @children[component].insert dn, value
36
+ else
37
+ @children[component].value = value
38
+ end
39
+ end
40
+ end
41
+
42
+ # Looks up a node and returns its value or raises
43
+ # LDAP::Server::Trie::NodeNotFoundError if it's not in the tree
44
+ def lookup(dn)
45
+ dn = LDAP::Server::DN.new(dn || '') if not dn.is_a? LDAP::Server::DN
46
+ return @value if dn.dname.empty?
47
+ component = dn.dname.pop
48
+ @children.each do |key, value|
49
+ if key.keys.first == component.keys.first
50
+ if key.values.first.start_with?(':') or key.values.first == component.values.first
51
+ return value.lookup dn
52
+ end
53
+ end
54
+ end
55
+ raise NodeNotFoundError
56
+ end
57
+
58
+ # Looks up a node and returns its value or the (non-nil) value of
59
+ # the nearest ancestor.
60
+ def match(dn, path = '')
61
+ dn = LDAP::Server::DN.new(dn || '') if not dn.is_a? LDAP::Server::DN
62
+ return path, @value if dn.dname.empty?
63
+ component = dn.dname.pop
64
+ @children.each do |key, value|
65
+ if key.keys.first == component.keys.first
66
+ if key.values.first.start_with?(':') or key.values.first == component.values.first
67
+ path.prepend ',' unless path.empty?
68
+ path.prepend "#{LDAP::Server::DN.join key}"
69
+ new_path, new_value = value.match dn, path
70
+ if new_value
71
+ return new_path, new_value
72
+ else
73
+ return (@value ? path : nil), @value
74
+ end
75
+ end
76
+ end
77
+ end
78
+ return path, @value
79
+ end
80
+
81
+ def print_tree(prefix = '')
82
+ if @value
83
+ p "#{prefix}{{#{@value}}}"
84
+ end
85
+ @children.each do |key, value|
86
+ p "#{prefix}#{key.keys.first} => #{key.values.first}"
87
+ @children[key].print_tree("#{prefix} ")
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,5 +1,5 @@
1
1
  module LDAP #:nodoc:
2
2
  class Server #:nodoc:
3
- VERSION = '0.5.3'
3
+ VERSION = '0.7.0'
4
4
  end
5
5
  end
@@ -7,21 +7,21 @@ Gem::Specification.new do |s|
7
7
  s.name = %q{ruby-ldapserver}
8
8
  s.version = LDAP::Server::VERSION
9
9
 
10
- s.authors = ["Brian Candler"]
10
+ s.authors = ["Brian Candler", "Florian Dejonckheere", "Lars Kanis"]
11
11
  s.description = %q{ruby-ldapserver is a lightweight, pure-Ruby skeleton for implementing LDAP server applications.}
12
- s.email = %q{B.Candler@pobox.com}
12
+ s.email = ["B.Candler@pobox.com", "florian@floriandejonckheere.be", "lars@greiz-reinsdorf.de"]
13
13
  s.files = `git ls-files`.split($/)
14
- s.homepage = %q{https://github.com/inscitiv/ruby-ldapserver}
15
- s.rdoc_options = ["--main", "README.txt"]
14
+ s.homepage = %q{https://github.com/larskanis/ruby-ldapserver}
15
+ s.rdoc_options = ["--main", "README.md"]
16
16
  s.require_paths = ["lib"]
17
17
  s.summary = %q{A pure-Ruby framework for building LDAP servers}
18
18
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
19
19
 
20
- s.required_ruby_version = '>= 1.9'
20
+ s.required_ruby_version = '>= 2.3'
21
21
 
22
- s.add_development_dependency 'bundler', '~> 1.3'
23
- s.add_development_dependency 'rake', '~> 10.0'
24
- s.add_development_dependency 'ruby-ldap', '~> 0.9.16' unless RUBY_PLATFORM == 'java'
25
- s.add_development_dependency 'jruby-ldap', '~> 0.0' if RUBY_PLATFORM == 'java'
22
+ s.add_development_dependency 'bundler', '>= 1.3', '< 3.0'
23
+ s.add_development_dependency 'rake', '~> 13.0'
24
+ s.add_development_dependency 'net-ldap', '~> 0.10'
26
25
  s.add_development_dependency 'rspec', '~> 3.1'
26
+ s.add_development_dependency 'test-unit', '~> 3.0'
27
27
  end
data/test/dn_test.rb ADDED
@@ -0,0 +1,149 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+ require 'ldap/server/dn'
3
+
4
+ class TestLdapDn < Test::Unit::TestCase
5
+ def setup
6
+ @dn = LDAP::Server::DN.new("cn=Steve Kille,o=Isode Limited,o=Companies,c=GB")
7
+ end
8
+
9
+ def test_find_first
10
+ assert_equal(nil, @dn.find_first("ou"))
11
+ assert_equal("Steve Kille", @dn.find_first("cn"))
12
+ assert_equal("Isode Limited", @dn.find_first("o"))
13
+ end
14
+
15
+ def test_find_last
16
+ assert_equal(nil, @dn.find_last("ou"))
17
+ assert_equal("Steve Kille", @dn.find_last("cn"))
18
+ assert_equal("Companies", @dn.find_last("o"))
19
+ end
20
+
21
+ def test_find
22
+ assert_equal([], @dn.find("ou"))
23
+ assert_equal(["Steve Kille"], @dn.find("cn"))
24
+ assert_equal(["Isode Limited", "Companies"], @dn.find("o"))
25
+ end
26
+
27
+ def test_find_nth
28
+ assert_equal(nil, @dn.find_nth("ou", 0))
29
+ assert_equal("Steve Kille", @dn.find_nth("cn", 0))
30
+ assert_equal(nil, @dn.find_nth("cn", 1))
31
+ assert_equal("Isode Limited", @dn.find_nth("o", 0))
32
+ assert_equal("Companies", @dn.find_nth("o", 1))
33
+ assert_equal(nil, @dn.find_nth("o", 2))
34
+ end
35
+
36
+ def test_start_with
37
+ assert @dn.start_with?("cn=Steve Kille")
38
+ assert @dn.start_with?("cn=Steve Kille, o=Isode Limited")
39
+ refute @dn.start_with?("cn=John Doe")
40
+ end
41
+
42
+ def test_start_with_format
43
+ assert @dn.start_with_format?("cn=Steve Kille")
44
+ assert @dn.start_with_format?("cn=foo, o=bar")
45
+ refute @dn.start_with_format?("c=GB")
46
+ refute @dn.start_with_format?("c=BE")
47
+ end
48
+
49
+ def test_end_with
50
+ assert @dn.end_with?("c=GB")
51
+ assert @dn.end_with?("o=Companies, c=GB")
52
+ refute @dn.end_with?("c=BE")
53
+ end
54
+
55
+ def test_end_with_format
56
+ assert @dn.end_with_format?("c=GB")
57
+ assert @dn.end_with_format?("o=foo, c=bar")
58
+ refute @dn.end_with_format?("cn=Steve Kille")
59
+ refute @dn.end_with_format?("cn=foo")
60
+ end
61
+
62
+ def test_equal
63
+ assert @dn.equal?("CN=Steve Kille, o=Isode Limited,O=Companies,c=GB")
64
+ refute @dn.equal?("cn=John Doe,o=Isode Limited,o=Companies,c=GB")
65
+ end
66
+
67
+ def test_equal_format
68
+ assert @dn.equal_format?("cn=Steve Kille,o=Isode Limited,o=Companies,c=GB")
69
+ assert @dn.equal_format?("cn=STEVE KILLE,o=ISODE LIMITED,o=COMPANIES,c=GB")
70
+ assert @dn.equal_format?("cn=foo,o=bar,o=baz,c=bat")
71
+ assert @dn.equal_format?("CN=foo,O=bar,O=baz,C=bat")
72
+ refute @dn.equal_format?("cn=foo,o=Isode Limited,c=GB")
73
+ end
74
+
75
+ def test_include
76
+ assert @dn.include?("cn=Steve Kille,o=Isode Limited,o=Companies,c=GB")
77
+ assert @dn.include?("cn=Steve Kille")
78
+ assert @dn.include?("o=Isode Limited")
79
+ assert @dn.include?("o=Isode Limited,o=Companies")
80
+ assert @dn.include?("c=GB")
81
+
82
+ refute @dn.include?("cn=John Doe,o=Isode Limited,o=Companies,c=GB")
83
+ refute @dn.include?("cn=Steve Kille,o=Isode Limited,c=GB")
84
+ end
85
+
86
+ def test_include_format
87
+ assert @dn.include_format?("cn=foo,o=bar,o=baz,c=bat")
88
+ assert @dn.include_format?("cn=foo")
89
+ assert @dn.include_format?("o=bar")
90
+ assert @dn.include_format?("o=bar,o=baz")
91
+ assert @dn.include_format?("c=bat")
92
+
93
+ refute @dn.include_format?("cn=foo,o=bar,c=bat")
94
+ refute @dn.include_format?("cn=bar,c=bat")
95
+ end
96
+
97
+ def test_parse
98
+ assert_equal @dn.parse("cn=:cn,o=:company,o=Companies,c=:country"),
99
+ {
100
+ :cn => 'Steve Kille',
101
+ :company => 'Isode Limited',
102
+ :country => 'GB'
103
+ }
104
+ assert_equal @dn.parse("cn=:cn,o=:company,o=:company,c=:country"),
105
+ {
106
+ :cn => 'Steve Kille',
107
+ :company => 'Companies',
108
+ :country => 'GB'
109
+ }
110
+ assert_empty @dn.parse("cn=Steve Kille,o=Isode Limited,o=Companies,c=GB")
111
+
112
+ assert_equal @dn.parse("cn=:cn,cn=Steve Kille,o=Isode Limited,o=Companies,c=GB"),
113
+ {
114
+ :cn => nil
115
+ }
116
+
117
+ assert_empty @dn.parse("o=Foo,o=Companies,c=GB")
118
+ assert_empty @dn.parse("cn=:cn,o=Foo,o=Companies,c=GB")
119
+
120
+ end
121
+
122
+ def test_each
123
+ answer = [
124
+ { 'cn' => 'Steve Kille' },
125
+ { 'o' => 'Isode Limited' },
126
+ { 'o' => 'Companies' },
127
+ { 'c' => 'GB' }
128
+ ]
129
+ i = 0
130
+ @dn.each do |pair|
131
+ assert_equal pair, answer[i]
132
+ i += 1
133
+ end
134
+ end
135
+
136
+ def test_reverse_each
137
+ answer = [
138
+ { 'c' => 'GB' },
139
+ { 'o' => 'Companies' },
140
+ { 'o' => 'Isode Limited' },
141
+ { 'cn' => 'Steve Kille' }
142
+ ]
143
+ i = 0
144
+ @dn.reverse_each do |pair|
145
+ assert_equal pair, answer[i]
146
+ i += 1
147
+ end
148
+ end
149
+ end