ruby-ldapserver 0.5.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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