ldumbd 0.1.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,64 @@
1
+ require 'socket'
2
+ require 'etc'
3
+
4
+ module Ldumbd
5
+ class PreforkServer
6
+ def initialize(opt)
7
+ @acceptor = nil
8
+ @bind_address = opt[:bindaddr] || '0.0.0.0'
9
+ @port = opt[:port] || 1389
10
+ @group = opt[:group]
11
+ @user = opt[:user]
12
+ @nodelay = opt.has_key?(:nodelay) ? opt[:nodelay] : true
13
+ @listen = opt[:listen] || 10
14
+ @num_processes = opt[:num_processes] || 10
15
+ @debug = opt[:debug]
16
+ end
17
+
18
+ def run(&block)
19
+ start
20
+ Process::GID.change_privilege(Etc.getgrnam(@group)) if @group
21
+ Process::UID.change_privilege(Etc.getpwnam(@user)) if @user
22
+ @num_processes.times do
23
+ fork do
24
+ trap('INT') { exit }
25
+
26
+ puts "child #$$ accepting connections on #{@bind_address}:#{@port}" if @debug
27
+ loop do
28
+ socket = @acceptor.accept
29
+ block.call(socket)
30
+ socket.close
31
+ end
32
+ exit
33
+ end
34
+ end
35
+ self
36
+ end
37
+
38
+ def join
39
+ Process.waitall
40
+ end
41
+
42
+ private
43
+ def start
44
+ @acceptor = TCPServer.new(@bind_address, @port)
45
+ if @nodelay
46
+ @acceptor.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
47
+ end
48
+ @acceptor.listen(@listen)
49
+ trap('EXIT') { @acceptor.close }
50
+ end
51
+ end
52
+ end
53
+
54
+ module LDAP
55
+ class Server
56
+ def run_preforkserver
57
+ opt = @opt
58
+ server = Ldumbd::PreforkServer.new(opt)
59
+ @thread = server.run do |socket|
60
+ LDAP::Server::Connection::new(socket, opt).handle_requests
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,89 @@
1
+ module Ldumbd
2
+ class TableMap
3
+ TABLE_MAP = {
4
+ group: {
5
+ object_classes: ['posixGroup'],
6
+ attributes: {
7
+ 'gidNumber' => :groups__id,
8
+ 'cn' => :groups__name,
9
+ 'memberUid' => :users
10
+ },
11
+ },
12
+ user: {
13
+ object_classes: ['posixAccount'],
14
+ attributes: {
15
+ 'uidNumber' => :users__id,
16
+ 'uid' => :users__name,
17
+ 'cn' => :users__realname,
18
+ 'loginShell' => :users__shell,
19
+ 'homeDirectory' => :users__homedir,
20
+ 'gidNumber' => :users__group_id
21
+ }
22
+ }
23
+ }
24
+
25
+ def self.invert_attribute_map(attributes)
26
+ attribute_array = attributes.map do |ldap_key, db_key|
27
+ /\A[a-z]*__(?<db_key_compact>.*)\z/ =~ db_key.to_s
28
+ [db_key_compact.nil? ? nil : db_key_compact.to_sym, ldap_key]
29
+ end
30
+ Hash[attribute_array].delete_if { |k, v| k.nil? }
31
+ end
32
+ private_class_method :invert_attribute_map
33
+
34
+ def self.invert_table_map(table_map)
35
+ table_array = table_map.map do |model, properties|
36
+ [model, invert_attribute_map(properties[:attributes])]
37
+ end
38
+ Hash[table_array]
39
+ end
40
+ private_class_method :invert_table_map
41
+
42
+ INVERSE_TABLE_MAP = invert_table_map(TABLE_MAP)
43
+
44
+ def self.db_key(model, ldap_key)
45
+ table = model_to_sym(model)
46
+ TABLE_MAP[table][:attributes][ldap_key]
47
+ end
48
+
49
+ def self.object_classes(model)
50
+ table = model_to_sym(model)
51
+ TABLE_MAP[table][:object_classes]
52
+ end
53
+
54
+ def self.model_to_sym(model)
55
+ model.name.downcase.to_sym
56
+ end
57
+ private_class_method :model_to_sym
58
+
59
+ def self.ldap_keys(model)
60
+ table = model_to_sym(model)
61
+ INVERSE_TABLE_MAP[table]
62
+ end
63
+
64
+ # TODO: move this into User/Group model class
65
+ def self.sequel_to_ldap_object(sequel_object)
66
+ # return unmodified object if it is not a Sequel model instance
67
+ return sequel_object unless sequel_object.is_a?(Sequel::Model)
68
+
69
+ model = sequel_object.class
70
+ ldap_keys = ldap_keys(model)
71
+ ldap_array = sequel_object.values.map do |sequel_key, value|
72
+ # LDAP::Server expects all values to be Arrays
73
+ [ldap_keys[sequel_key], [value]]
74
+ end
75
+ ldap_object = Hash[ldap_array]
76
+
77
+ ldap_object['objectClass'] = object_classes(model)
78
+ if sequel_object.is_a?(Group)
79
+ ldap_object['dn_prefix'] = "cn=#{sequel_object.name},ou=Groups"
80
+ if sequel_object.users.any?
81
+ ldap_object['memberUid'] = sequel_object.users.map { |u| u.name }
82
+ end
83
+ elsif sequel_object.is_a?(User)
84
+ ldap_object['dn_prefix'] = "uid=#{sequel_object.name},ou=People"
85
+ end
86
+ ldap_object
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,67 @@
1
+ require 'ldap/server'
2
+ require 'sequel'
3
+ require 'ldumbd/filter_converter'
4
+
5
+ module Ldumbd
6
+ class TreeObject
7
+ attr_accessor :children
8
+ attr_accessor :attributes
9
+
10
+ def initialize(attributes = {})
11
+ @attributes = attributes
12
+ @children = []
13
+ end
14
+
15
+ def matches_filter?(filter)
16
+ ldap_object = TableMap.sequel_to_ldap_object(@attributes)
17
+ LDAP::Server::Filter.run(filter, ldap_object)
18
+ end
19
+
20
+ def each_child(filter = [:true], recurse = false, &block)
21
+ @children.each do |child|
22
+ if child.is_a?(TreeObject)
23
+ if LDAP::Server::Filter.run(filter, child.attributes)
24
+ yield child.attributes
25
+ end
26
+ child.each_child(filter, recurse, &block) if recurse
27
+ elsif child < Sequel::Model
28
+ query = Ldumbd::FilterConverter.filter_to_sequel(child, filter)
29
+ child.where(query).each do |r|
30
+ yield r
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.by_dn(basedn, dn)
37
+ object = nil
38
+
39
+ dn_filter, model = dn_to_filter_and_model(basedn, dn)
40
+ if dn_filter
41
+ query = Ldumbd::FilterConverter.filter_to_sequel(model, dn_filter)
42
+ result = model.where(query).first
43
+ object = result ? self.new(result) : nil
44
+ end
45
+ object
46
+ end
47
+
48
+ def self.attribute_from_dn(basedn, dn, attribute, ou)
49
+ dn.scan(/\A#{attribute}=([^,]*),ou=#{ou},#{basedn}\z/).flatten.last
50
+ end
51
+ private_class_method :attribute_from_dn
52
+
53
+ def self.dn_to_filter_and_model(basedn, dn)
54
+ uid = attribute_from_dn(basedn, dn, 'uid', 'People')
55
+ cn = attribute_from_dn(basedn, dn, 'cn', 'Groups')
56
+
57
+ if uid
58
+ return [:eq, 'uid', nil, uid], User
59
+ elsif cn
60
+ return [:eq, 'cn', nil, cn], Group
61
+ else
62
+ return nil, nil
63
+ end
64
+ end
65
+ private_class_method :dn_to_filter_and_model
66
+ end
67
+ end
@@ -0,0 +1,3 @@
1
+ class User < Sequel::Model
2
+ many_to_many :groups
3
+ end
@@ -0,0 +1,3 @@
1
+ module Ldumbd
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,154 @@
1
+ require_relative 'spec_helper'
2
+
3
+ require 'ldumbd/filter_converter'
4
+
5
+ describe Ldumbd::FilterConverter do
6
+ before(:each) do
7
+ @john = new_user('John Doe')
8
+ @jane = new_user('Jane Doe')
9
+ @all_group = Group.create(name: 'allusers')
10
+ @all_group.add_user(@john)
11
+ @all_group.add_user(@jane)
12
+ end
13
+
14
+ it 'should process :true filters' do
15
+ filter = [:true]
16
+ user_query = Ldumbd::FilterConverter.filter_to_sequel(User, filter)
17
+ group_query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
18
+ users = User.where(user_query)
19
+ groups = Group.where(group_query)
20
+ users.count.must_equal User.count
21
+ groups.count.must_equal Group.count
22
+ end
23
+
24
+ it 'should process :false filters' do
25
+ filter = [:false]
26
+ user_query = Ldumbd::FilterConverter.filter_to_sequel(User, filter)
27
+ group_query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
28
+ users = User.where(user_query)
29
+ groups = Group.where(group_query)
30
+ users.count.must_equal 0
31
+ groups.count.must_equal 0
32
+ end
33
+
34
+ it 'should process :eq filters' do
35
+ filter = [:eq, 'uid', nil, @john.name]
36
+ query = Ldumbd::FilterConverter.filter_to_sequel(User, filter)
37
+ users = User.where(query)
38
+ users.count.must_equal 1
39
+ users.first.must_equal @john
40
+ end
41
+
42
+ it 'should process :or filters' do
43
+ filter = [:or,
44
+ [:eq, 'uid', nil, @john.name],
45
+ [:eq, 'uid', nil, @jane.name]]
46
+ query = Ldumbd::FilterConverter.filter_to_sequel(User, filter)
47
+ users = User.where(query).order(:id)
48
+ users.count.must_equal 2
49
+ users.all[0].must_equal @john
50
+ users.all[1].must_equal @jane
51
+ end
52
+
53
+ it 'should process memberUid filters' do
54
+ filter = [:eq, 'memberUid', nil, @jane.name]
55
+ query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
56
+ groups = Group.where(query)
57
+ groups.count.must_equal 1
58
+ groups.first.must_equal @all_group
59
+ end
60
+
61
+ it 'should process :and filters' do
62
+ filter = [:and,
63
+ [:eq, 'gidNumber', nil, @all_group.id],
64
+ [:eq, 'memberUid', nil, @jane.name],
65
+ [:not, [:eq, 'memberUid', nil, 'nonexistentusername42']]]
66
+ query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
67
+ groups = Group.where(query)
68
+ groups.count.must_equal 1
69
+ groups.first.must_equal @all_group
70
+ end
71
+
72
+ it 'should process :ge and :le filters' do
73
+ filter = [:and,
74
+ [:ge, 'gidNumber', nil, 65000],
75
+ [:le, 'gidNumber', nil, 66000]]
76
+ query = Ldumbd::FilterConverter.filter_to_sequel(User, filter)
77
+ users = User.where(query).order(:id)
78
+ users.count.must_equal 2
79
+ users.all[0].must_equal @john
80
+ users.all[1].must_equal @jane
81
+ end
82
+
83
+ it 'should process :substrings filters' do
84
+ filter = [:substrings, 'uid', nil, "jo", "doe"]
85
+ query = Ldumbd::FilterConverter.filter_to_sequel(User, filter)
86
+ users = User.where(query)
87
+ users.count.must_equal 1
88
+ users.first.must_equal @john
89
+ end
90
+
91
+ it 'should process :substrings filters on memberUids' do
92
+ filter = [:substrings, 'memberUid', nil, "jo", "doe"]
93
+ query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
94
+ groups = Group.where(query)
95
+ groups.count.must_equal 1
96
+ groups.first.must_equal @all_group
97
+ end
98
+
99
+ it 'should return no results for unknown attributes' do
100
+ filter = [:eq, 'nonexistent', nil, 'somethingsomething']
101
+ query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
102
+ groups = Group.where(query)
103
+ groups.count.must_equal 0
104
+ end
105
+
106
+ it 'should process objectClass :substrings requests' do
107
+ # (objectClass=posixGr*)
108
+ filter = [:substrings, 'objectClass', nil, 'posixGr', nil]
109
+ query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
110
+ groups = Group.where(query)
111
+ groups.count.must_equal Group.count
112
+ end
113
+
114
+ it 'should process objectClass :eq requests' do
115
+ # (objectClass=posixGroup)
116
+ filter = [:eq, 'objectClass', nil, 'posixGroup']
117
+ query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
118
+ groups = Group.where(query)
119
+ groups.count.must_equal Group.count
120
+ end
121
+
122
+ it 'should process objectClass :le requests' do
123
+ # (objectClass<=posixGroupZZZ)
124
+ filter = [:le, 'objectClass', nil, 'posixGroupZZZ']
125
+ query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
126
+ groups = Group.where(query)
127
+ groups.count.must_equal Group.count
128
+ end
129
+
130
+ it 'should process objectClass :ge requests' do
131
+ # (objectClass>=posixGroupGaaah)
132
+ filter = [:ge, 'objectClass', nil, 'posixGaaah']
133
+ query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
134
+ groups = Group.where(query)
135
+ groups.count.must_equal Group.count
136
+ end
137
+
138
+ it 'should process :present filters' do
139
+ # (homeDirectory=*)
140
+ filter = [:present, 'homeDirectory']
141
+ query = Ldumbd::FilterConverter.filter_to_sequel(User, filter)
142
+ users = User.where(query)
143
+ users.count.must_equal User.count
144
+ end
145
+
146
+ it 'should process :present filters on memberUids' do
147
+ # (memberUid=*)
148
+ filter = [:present, 'memberUid']
149
+ query = Ldumbd::FilterConverter.filter_to_sequel(Group, filter)
150
+ groups = Group.where(query)
151
+ groups_with_members = Group.where(users: User.all)
152
+ groups.count.must_equal groups_with_members.count
153
+ end
154
+ end
@@ -0,0 +1,274 @@
1
+ require_relative 'spec_helper'
2
+
3
+ require 'ldumbd/table_map'
4
+ require 'ldumbd/ldap_tree'
5
+ require 'ldumbd/operation'
6
+
7
+ class Ldumbd::Operation
8
+ public :search_results
9
+ end
10
+
11
+ class ConnectionMock
12
+ def opt
13
+ Hash.new
14
+ end
15
+ end
16
+
17
+ class MessageIDMock; end
18
+
19
+ def ldap_objects(sequel_objects)
20
+ sequel_objects.map { |o| Ldumbd::TableMap.sequel_to_ldap_object(o) }
21
+ end
22
+
23
+ describe Ldumbd::Operation do
24
+ before(:each) do
25
+ @john = new_user('John Doe')
26
+ @jane = new_user('Jane Doe')
27
+ @no_group = Group.where(id: 65534, name: 'nogroup').first
28
+ @all_group = Group.create(name: 'allusers')
29
+ @all_group.add_user(@john)
30
+ @all_group.add_user(@jane)
31
+
32
+ @basedn = 'dc=example,dc=org'
33
+ @root = {
34
+ 'dn_prefix' => '',
35
+ 'dc' => 'example',
36
+ 'objectClass' => %w{top domain}
37
+ }
38
+ @people = {
39
+ 'dn_prefix' => 'ou=People',
40
+ 'ou' => 'People',
41
+ 'objectClass' => %w{top organizationalUnit}
42
+ }
43
+ @groups = {
44
+ 'dn_prefix' => 'ou=Groups',
45
+ 'ou' => 'Groups',
46
+ 'objectClass' => %w{top organizationalUnit}
47
+ }
48
+ @operation = Ldumbd::Operation.new(ConnectionMock.new,
49
+ MessageIDMock.new,
50
+ Ldumbd::LdapTree.new(@basedn))
51
+ end
52
+
53
+ it 'should raise exceptions when using an invalid search scope' do
54
+ proc do
55
+ @operation.search_results(nil, 'InvalidScope', nil, nil)
56
+ end.must_raise LDAP::ResultError::UnwillingToPerform
57
+ end
58
+
59
+ describe LDAP::Server::BaseObject do
60
+ before(:each) do
61
+ @scope = LDAP::Server::BaseObject
62
+ end
63
+
64
+ it 'should find users by dn' do
65
+ filter = [:true]
66
+ dn = "uid=#{@john.name},ou=People,#{@basedn}"
67
+ results = []
68
+ @operation.search_results(dn, @scope, nil, filter) do |r|
69
+ results << r
70
+ end
71
+ results.must_equal ldap_objects([@john])
72
+ end
73
+
74
+ it 'should find organization units by dn' do
75
+ filter = [:true]
76
+ dn = "ou=People,#{@basedn}"
77
+ results = []
78
+ @operation.search_results(dn, @scope, nil, filter) do |r|
79
+ results << r
80
+ end
81
+ results.must_equal [@people]
82
+ end
83
+
84
+ it 'should find the root object by dn' do
85
+ filter = [:true]
86
+ dn = @basedn
87
+ results = []
88
+ @operation.search_results(dn, @scope, nil, filter) do |r|
89
+ results << r
90
+ end
91
+ results.must_equal [@root]
92
+ end
93
+
94
+ it 'should raise exceptions when searching for nonexistent objects' do
95
+ filter = [:true]
96
+ ["dc=non,dc=existent",
97
+ "uid=nonexistent,ou=People,#{@basedn}"].each do |dn|
98
+ proc do
99
+ @operation.search_results(dn, @scope, nil, filter)
100
+ end.must_raise LDAP::ResultError::NoSuchObject
101
+ end
102
+ end
103
+
104
+ it 'should not return any objects when using unsatisfiable filters' do
105
+ filter = [:eq, 'nonexistent_attribute', nil, 'nonexistent_value']
106
+ dn = @basedn
107
+ results = []
108
+ @operation.search_results(dn, @scope, nil, filter) do |r|
109
+ results << r
110
+ end
111
+ results.must_equal []
112
+ end
113
+ end
114
+
115
+ describe LDAP::Server::SingleLevel do
116
+ before(:each) do
117
+ @scope = LDAP::Server::SingleLevel
118
+ end
119
+
120
+ it 'should retrieve the immediate children of an object' do
121
+ filter = [:true]
122
+ dn = @basedn
123
+ results = []
124
+ @operation.search_results(dn, @scope, nil, filter) do |r|
125
+ results << r
126
+ end
127
+ subtree = [@people, @groups]
128
+ results.must_equal_unordered subtree
129
+ end
130
+
131
+ it 'should retrieve all users via the People ou' do
132
+ filter = [:true]
133
+ dn = "ou=People,#{@basedn}"
134
+ results = []
135
+ @operation.search_results(dn, @scope, nil, filter) do |r|
136
+ results << r
137
+ end
138
+ people_tree = [@john, @jane]
139
+ results.must_equal_unordered ldap_objects(people_tree)
140
+ end
141
+
142
+ it 'should retrieve all users via the People ou when using a filter' do
143
+ filter = [:eq, 'objectClass', nil, 'posixAccount']
144
+ dn = "ou=People,#{@basedn}"
145
+ results = []
146
+ @operation.search_results(dn, @scope, nil, filter) do |r|
147
+ results << r
148
+ end
149
+ people_tree = [@john, @jane]
150
+ results.must_equal_unordered ldap_objects(people_tree)
151
+ end
152
+
153
+ it 'should raise exceptions when searching for nonexistent objects' do
154
+ filter = [:true]
155
+ ["dc=non,dc=existent",
156
+ "uid=nonexistent,ou=People,#{@basedn}"].each do |dn|
157
+ proc do
158
+ @operation.search_results(dn, @scope, nil, filter)
159
+ end.must_raise LDAP::ResultError::NoSuchObject
160
+ end
161
+ end
162
+
163
+ it 'should not return any objects when using unsatisfiable filters' do
164
+ filter = [:eq, 'nonexistent_attribute', nil, 'nonexistent_value']
165
+ dn = @basedn
166
+ results = []
167
+ @operation.search_results(dn, @scope, nil, filter) do |r|
168
+ results << r
169
+ end
170
+ results.must_equal []
171
+ end
172
+ end
173
+
174
+ describe LDAP::Server::WholeSubtree do
175
+ before(:each) do
176
+ @scope = LDAP::Server::WholeSubtree
177
+ end
178
+
179
+ it 'should retrieve the whole tree via the basedn' do
180
+ filter = [:true]
181
+ dn = @basedn
182
+ results = []
183
+ @operation.search_results(dn, @scope, nil, filter) do |r|
184
+ results << r
185
+ end
186
+ ldap_tree = [@root, @people, @john, @jane, @groups, @no_group, @all_group]
187
+ results.must_equal_unordered ldap_objects(ldap_tree)
188
+ end
189
+
190
+ it 'should include the base object in the results when using filters' do
191
+ filter = [:eq, 'uid', nil, @john.name]
192
+ dn = "uid=#{@john.name},ou=People,#{@basedn}"
193
+ results = []
194
+ @operation.search_results(dn, @scope, nil, filter) do |r|
195
+ results << r
196
+ end
197
+ results.must_equal ldap_objects([@john])
198
+ end
199
+
200
+ it 'should retrieve all users via the People ou' do
201
+ filter = [:true]
202
+ dn = "ou=People,#{@basedn}"
203
+ results = []
204
+ @operation.search_results(dn, @scope, nil, filter) do |r|
205
+ results << r
206
+ end
207
+ people_tree = [@people, @john, @jane]
208
+ results.must_equal_unordered ldap_objects(people_tree)
209
+ end
210
+
211
+ it 'should retrieve all users via the People ou when using a filter' do
212
+ filter = [:eq, 'objectClass', nil, 'posixAccount']
213
+ dn = "ou=People,#{@basedn}"
214
+ results = []
215
+ @operation.search_results(dn, @scope, nil, filter) do |r|
216
+ results << r
217
+ end
218
+ people_tree = [@john, @jane]
219
+ results.must_equal_unordered ldap_objects(people_tree)
220
+ end
221
+
222
+ it 'should retrieve single users by dn' do
223
+ filter = [:true]
224
+ dn = "uid=#{@john.name},ou=People,#{@basedn}"
225
+ results = []
226
+ @operation.search_results(dn, @scope, nil, filter) do |r|
227
+ results << r
228
+ end
229
+ results.must_equal ldap_objects([@john])
230
+ end
231
+
232
+ it 'should raise exceptions when searching for nonexistent objects' do
233
+ filter = [:true]
234
+ ["dc=non,dc=existent",
235
+ "uid=nonexistent,ou=People,#{@basedn}"].each do |dn|
236
+ proc do
237
+ @operation.search_results(dn, @scope, nil, filter)
238
+ end.must_raise LDAP::ResultError::NoSuchObject
239
+ end
240
+ end
241
+
242
+ it 'should not return any objects when using unsatisfiable filters' do
243
+ filter = [:eq, 'nonexistent_attribute', nil, 'nonexistent_value']
244
+ dn = @basedn
245
+ results = []
246
+ @operation.search_results(dn, @scope, nil, filter) do |r|
247
+ results << r
248
+ end
249
+ results.must_equal []
250
+ end
251
+ end
252
+
253
+ describe 'search' do
254
+ it 'should call send_SearchResultEntry' do
255
+ filter = [:true]
256
+ scope = LDAP::Server::BaseObject
257
+ mock = MiniTest::Mock.new
258
+
259
+ root_dn = @basedn
260
+ root_avs = @root.reject { |k, v| k == 'dn_prefix' }
261
+ john_dn = "uid=#{@john.name},ou=People,#{@basedn}"
262
+ john_attrs = Ldumbd::TableMap.sequel_to_ldap_object(@john)
263
+ john_avs = john_attrs.reject { |k, v| k == 'dn_prefix' }
264
+
265
+ mock.expect(:send_SearchResultEntry, nil, [root_dn, root_avs])
266
+ mock.expect(:send_SearchResultEntry, nil, [john_dn, john_avs])
267
+ @operation.stub(:send_SearchResultEntry,
268
+ ->(dn, avs) { mock.send_SearchResultEntry(dn, avs) }) do
269
+ @operation.search(root_dn, scope, nil, filter)
270
+ @operation.search(john_dn, scope, nil, filter)
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,41 @@
1
+ require 'minitest/autorun'
2
+ require 'yaml'
3
+ require 'sequel'
4
+
5
+ DATABASE_URL = ENV['DATABASE_URL'] || 'sqlite://ldumbd-test.sqlite3'
6
+ DB = Sequel.connect(DATABASE_URL)
7
+
8
+ require 'ldumbd/user'
9
+ require 'ldumbd/group'
10
+
11
+ class MiniTest::Spec
12
+ before :each do
13
+ @db_transaction = Fiber.new do
14
+ DB.transaction(savepoint: true, rollback: :always) { Fiber.yield }
15
+ end
16
+ @db_transaction.resume
17
+ end
18
+
19
+ after :each do
20
+ @db_transaction.resume
21
+ end
22
+ end
23
+
24
+ def new_user(realname)
25
+ name = realname.downcase.gsub(' ', '')
26
+ homedir = "/home/#{name}"
27
+ User.create(name: name,
28
+ homedir: homedir,
29
+ realname: realname)
30
+ end
31
+
32
+ module MiniTest::Assertions
33
+ def assert_equal_unordered(a, b, msg = nil)
34
+ msg ||= "Expected #{mu_pp(a)} to be equivalent to #{mu_pp(b)}"
35
+ freq_a = a.inject(Hash.new(0)) { |h, v| h[v] += 1; h }
36
+ freq_b = b.inject(Hash.new(0)) { |h, v| h[v] += 1; h }
37
+ assert(freq_a == freq_b, msg)
38
+ end
39
+ end
40
+
41
+ Array.infect_an_assertion :assert_equal_unordered, :must_equal_unordered