treequel 1.0.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.
- data/ChangeLog +354 -0
- data/LICENSE +27 -0
- data/README +66 -0
- data/Rakefile +345 -0
- data/Rakefile.local +43 -0
- data/bin/treeirb +14 -0
- data/bin/treequel +229 -0
- data/examples/company-directory.rb +112 -0
- data/examples/ldap-monitor.rb +143 -0
- data/examples/ldap-monitor/public/css/master.css +328 -0
- data/examples/ldap-monitor/public/images/card_small.png +0 -0
- data/examples/ldap-monitor/public/images/chain_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
- data/examples/ldap-monitor/public/images/plug.png +0 -0
- data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
- data/examples/ldap-monitor/public/images/tick.png +0 -0
- data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
- data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
- data/examples/ldap-monitor/views/backends.erb +41 -0
- data/examples/ldap-monitor/views/connections.erb +74 -0
- data/examples/ldap-monitor/views/databases.erb +39 -0
- data/examples/ldap-monitor/views/dump_subsystem.erb +14 -0
- data/examples/ldap-monitor/views/index.erb +14 -0
- data/examples/ldap-monitor/views/layout.erb +35 -0
- data/examples/ldap-monitor/views/listeners.erb +30 -0
- data/examples/ldap_state.rb +62 -0
- data/lib/treequel.rb +145 -0
- data/lib/treequel/branch.rb +589 -0
- data/lib/treequel/branchcollection.rb +204 -0
- data/lib/treequel/branchset.rb +360 -0
- data/lib/treequel/constants.rb +604 -0
- data/lib/treequel/directory.rb +541 -0
- data/lib/treequel/exceptions.rb +32 -0
- data/lib/treequel/filter.rb +704 -0
- data/lib/treequel/mixins.rb +325 -0
- data/lib/treequel/schema.rb +245 -0
- data/lib/treequel/schema/attributetype.rb +252 -0
- data/lib/treequel/schema/ldapsyntax.rb +96 -0
- data/lib/treequel/schema/matchingrule.rb +124 -0
- data/lib/treequel/schema/matchingruleuse.rb +124 -0
- data/lib/treequel/schema/objectclass.rb +289 -0
- data/lib/treequel/sequel_integration.rb +26 -0
- data/lib/treequel/utils.rb +169 -0
- data/rake/191_compat.rb +26 -0
- data/rake/dependencies.rb +76 -0
- data/rake/helpers.rb +434 -0
- data/rake/hg.rb +261 -0
- data/rake/manual.rb +782 -0
- data/rake/packaging.rb +135 -0
- data/rake/publishing.rb +318 -0
- data/rake/rdoc.rb +30 -0
- data/rake/style.rb +62 -0
- data/rake/svn.rb +668 -0
- data/rake/testing.rb +187 -0
- data/rake/verifytask.rb +64 -0
- data/rake/win32.rb +190 -0
- data/spec/lib/constants.rb +93 -0
- data/spec/lib/helpers.rb +100 -0
- data/spec/treequel/branch_spec.rb +569 -0
- data/spec/treequel/branchcollection_spec.rb +213 -0
- data/spec/treequel/branchset_spec.rb +376 -0
- data/spec/treequel/directory_spec.rb +487 -0
- data/spec/treequel/filter_spec.rb +482 -0
- data/spec/treequel/mixins_spec.rb +330 -0
- data/spec/treequel/schema/attributetype_spec.rb +237 -0
- data/spec/treequel/schema/ldapsyntax_spec.rb +83 -0
- data/spec/treequel/schema/matchingrule_spec.rb +158 -0
- data/spec/treequel/schema/matchingruleuse_spec.rb +137 -0
- data/spec/treequel/schema/objectclass_spec.rb +262 -0
- data/spec/treequel/schema_spec.rb +118 -0
- data/spec/treequel/utils_spec.rb +49 -0
- data/spec/treequel_spec.rb +179 -0
- metadata +169 -0
data/spec/lib/helpers.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
BEGIN {
|
5
|
+
require 'pathname'
|
6
|
+
basedir = Pathname.new( __FILE__ ).dirname.parent
|
7
|
+
|
8
|
+
libdir = basedir + "lib"
|
9
|
+
|
10
|
+
$LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
|
11
|
+
}
|
12
|
+
|
13
|
+
begin
|
14
|
+
require 'yaml'
|
15
|
+
require 'treequel'
|
16
|
+
|
17
|
+
require 'spec/lib/constants'
|
18
|
+
rescue LoadError
|
19
|
+
unless Object.const_defined?( :Gem )
|
20
|
+
require 'rubygems'
|
21
|
+
retry
|
22
|
+
end
|
23
|
+
raise
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
### RSpec helper functions.
|
28
|
+
module Treequel::SpecHelpers
|
29
|
+
include Treequel::TestConstants
|
30
|
+
|
31
|
+
### Make an easily-comparable version vector out of +ver+ and return it.
|
32
|
+
def vvec( ver )
|
33
|
+
return ver.split('.').collect {|char| char.to_i }.pack('N*')
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
class ArrayLogger
|
38
|
+
### Create a new ArrayLogger that will append content to +array+.
|
39
|
+
def initialize( array )
|
40
|
+
@array = array
|
41
|
+
end
|
42
|
+
|
43
|
+
### Write the specified +message+ to the array.
|
44
|
+
def write( message )
|
45
|
+
@array << message
|
46
|
+
end
|
47
|
+
|
48
|
+
### No-op -- this is here just so Logger doesn't complain
|
49
|
+
def close; end
|
50
|
+
|
51
|
+
end # class ArrayLogger
|
52
|
+
|
53
|
+
|
54
|
+
unless defined?( LEVEL )
|
55
|
+
LEVEL = {
|
56
|
+
:debug => Logger::DEBUG,
|
57
|
+
:info => Logger::INFO,
|
58
|
+
:warn => Logger::WARN,
|
59
|
+
:error => Logger::ERROR,
|
60
|
+
:fatal => Logger::FATAL,
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
###############
|
65
|
+
module_function
|
66
|
+
###############
|
67
|
+
|
68
|
+
### Reset the logging subsystem to its default state.
|
69
|
+
def reset_logging
|
70
|
+
Treequel.reset_logger
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
### Alter the output of the default log formatter to be pretty in SpecMate output
|
75
|
+
def setup_logging( level=Logger::FATAL )
|
76
|
+
|
77
|
+
# Turn symbol-style level config into Logger's expected Fixnum level
|
78
|
+
if Treequel::Loggable::LEVEL.key?( level )
|
79
|
+
level = Treequel::Loggable::LEVEL[ level ]
|
80
|
+
end
|
81
|
+
|
82
|
+
logger = Logger.new( $stderr )
|
83
|
+
Treequel.logger = logger
|
84
|
+
Treequel.logger.level = level
|
85
|
+
|
86
|
+
# Only do this when executing from a spec in TextMate
|
87
|
+
if ENV['HTML_LOGGING'] || (ENV['TM_FILENAME'] && ENV['TM_FILENAME'] =~ /_spec\.rb/)
|
88
|
+
Thread.current['logger-output'] = []
|
89
|
+
logdevice = ArrayLogger.new( Thread.current['logger-output'] )
|
90
|
+
Treequel.logger = Logger.new( logdevice )
|
91
|
+
# Treequel.logger.level = level
|
92
|
+
Treequel.logger.formatter = Treequel::HtmlLogFormatter.new( logger )
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
# vim: set nosta noet ts=4 sw=4:
|
100
|
+
|
@@ -0,0 +1,569 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
BEGIN {
|
4
|
+
require 'pathname'
|
5
|
+
basedir = Pathname.new( __FILE__ ).dirname.parent.parent
|
6
|
+
|
7
|
+
libdir = basedir + "lib"
|
8
|
+
|
9
|
+
$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
|
10
|
+
}
|
11
|
+
|
12
|
+
begin
|
13
|
+
require 'spec'
|
14
|
+
require 'spec/lib/constants'
|
15
|
+
require 'spec/lib/helpers'
|
16
|
+
|
17
|
+
require 'treequel/branch'
|
18
|
+
require 'treequel/branchset'
|
19
|
+
require 'treequel/branchcollection'
|
20
|
+
rescue LoadError
|
21
|
+
unless Object.const_defined?( :Gem )
|
22
|
+
require 'rubygems'
|
23
|
+
retry
|
24
|
+
end
|
25
|
+
raise
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
include Treequel::TestConstants
|
30
|
+
include Treequel::Constants
|
31
|
+
|
32
|
+
#####################################################################
|
33
|
+
### C O N T E X T S
|
34
|
+
#####################################################################
|
35
|
+
|
36
|
+
describe Treequel::Branch do
|
37
|
+
include Treequel::SpecHelpers
|
38
|
+
|
39
|
+
before( :all ) do
|
40
|
+
setup_logging( :fatal )
|
41
|
+
end
|
42
|
+
|
43
|
+
after( :all ) do
|
44
|
+
reset_logging()
|
45
|
+
end
|
46
|
+
|
47
|
+
before( :each ) do
|
48
|
+
@directory = mock( "treequel directory", :get_entry => :an_entry_hash )
|
49
|
+
end
|
50
|
+
|
51
|
+
after( :each ) do
|
52
|
+
Treequel::Branch.include_operational_attrs = false
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
it "can be constructed from a DN" do
|
57
|
+
branch = Treequel::Branch.new( @directory, TEST_PEOPLE_DN )
|
58
|
+
branch.dn.should == TEST_PEOPLE_DN
|
59
|
+
end
|
60
|
+
|
61
|
+
it "raises an exception if created with an invalid DN" do
|
62
|
+
expect {
|
63
|
+
Treequel::Branch.new(@directory, 'soapyfinger')
|
64
|
+
}.to raise_error( ArgumentError, /invalid dn/i )
|
65
|
+
end
|
66
|
+
|
67
|
+
it "can be constructed from an entry returned from LDAP::Conn.search_ext2" do
|
68
|
+
entry = {
|
69
|
+
'dn' => [TEST_PERSON_DN],
|
70
|
+
TEST_PERSON_DN_ATTR => TEST_PERSON_DN_VALUE,
|
71
|
+
}
|
72
|
+
branch = Treequel::Branch.new_from_entry( entry, @directory )
|
73
|
+
|
74
|
+
branch.rdn_attributes.should == { TEST_PERSON_DN_ATTR => [TEST_PERSON_DN_VALUE] }
|
75
|
+
branch.entry.should == entry
|
76
|
+
end
|
77
|
+
|
78
|
+
it "can be configured to include operational attributes for all future instances" do
|
79
|
+
Treequel::Branch.include_operational_attrs = false
|
80
|
+
Treequel::Branch.new( @directory, TEST_PEOPLE_DN ).include_operational_attrs?.should be_false
|
81
|
+
Treequel::Branch.include_operational_attrs = true
|
82
|
+
Treequel::Branch.new( @directory, TEST_PEOPLE_DN ).include_operational_attrs?.should be_true
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
describe "instances" do
|
87
|
+
|
88
|
+
before( :each ) do
|
89
|
+
@branch = Treequel::Branch.new( @directory, TEST_HOSTS_DN )
|
90
|
+
|
91
|
+
@schema = mock( "treequel schema" )
|
92
|
+
@entry = mock( "entry object" )
|
93
|
+
@directory.stub!( :schema ).and_return( @schema )
|
94
|
+
@directory.stub!( :get_entry ).and_return( @entry )
|
95
|
+
@directory.stub!( :base_dn ).and_return( TEST_BASE_DN )
|
96
|
+
@schema.stub!( :attribute_types ).
|
97
|
+
and_return({ :cn => :a_value, :ou => :a_value })
|
98
|
+
|
99
|
+
@attribute_type = mock( "schema attribute type object" )
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
it "knows what its RDN is" do
|
104
|
+
@branch.rdn.should == TEST_HOSTS_RDN
|
105
|
+
end
|
106
|
+
|
107
|
+
it "knows what its DN is" do
|
108
|
+
@branch.dn.should == TEST_HOSTS_DN
|
109
|
+
end
|
110
|
+
|
111
|
+
it "can return its DN as an array of attribute=value pairs" do
|
112
|
+
@branch.split_dn.should == TEST_HOSTS_DN.split(/\s*,\s*/)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "can return its DN as a limited array of attribute=value pairs" do
|
116
|
+
@branch.split_dn( 2 ).should have( 2 ).members
|
117
|
+
@branch.split_dn( 2 ).should include( TEST_HOSTS_RDN, TEST_BASE_DN )
|
118
|
+
end
|
119
|
+
|
120
|
+
it "are Comparable if they are siblings" do
|
121
|
+
sibling = Treequel::Branch.new( @directory, TEST_PEOPLE_DN )
|
122
|
+
|
123
|
+
( @branch <=> sibling ).should == -1
|
124
|
+
( sibling <=> @branch ).should == 1
|
125
|
+
( @branch <=> @branch ).should == 0
|
126
|
+
end
|
127
|
+
|
128
|
+
it "are Comparable if they are parent and child" do
|
129
|
+
child = Treequel::Branch.new( @directory, TEST_HOST_DN )
|
130
|
+
|
131
|
+
( @branch <=> child ).should == 1
|
132
|
+
( child <=> @branch ).should == -1
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
it "fetch their LDAP::Entry from the directory if they don't already have one" do
|
137
|
+
@directory.should_receive( :get_entry ).with( @branch ).exactly( :once ).
|
138
|
+
and_return( :the_entry )
|
139
|
+
|
140
|
+
@branch.entry.should == :the_entry
|
141
|
+
@branch.entry.should == :the_entry # this should fetch the cached one
|
142
|
+
end
|
143
|
+
|
144
|
+
it "fetch their LDAP::Entry with operational attributes if include_operational_attrs is set" do
|
145
|
+
@branch.include_operational_attrs = true
|
146
|
+
@directory.should_not_receive( :get_entry )
|
147
|
+
@directory.should_receive( :get_extended_entry ).with( @branch ).exactly( :once ).
|
148
|
+
and_return( :the_extended_entry )
|
149
|
+
|
150
|
+
@branch.entry.should == :the_extended_entry
|
151
|
+
end
|
152
|
+
|
153
|
+
it "clears any cached values if its include_operational_attrs attribute is changed" do
|
154
|
+
@directory.should_receive( :get_entry ).with( @branch ).exactly( :once ).
|
155
|
+
and_return( :the_entry )
|
156
|
+
@directory.should_receive( :get_extended_entry ).with( @branch ).exactly( :once ).
|
157
|
+
and_return( :the_extended_entry )
|
158
|
+
|
159
|
+
@branch.entry.should == :the_entry
|
160
|
+
@branch.include_operational_attrs = true
|
161
|
+
@branch.entry.should == :the_extended_entry
|
162
|
+
end
|
163
|
+
|
164
|
+
it "returns a human-readable representation of itself for #inspect" do
|
165
|
+
@directory.should_not_receive( :get_entry ) # shouldn't try to load the entry for #inspect
|
166
|
+
|
167
|
+
rval = @branch.inspect
|
168
|
+
|
169
|
+
rval.should =~ /#{TEST_HOSTS_DN_ATTR}/i
|
170
|
+
rval.should =~ /#{TEST_HOSTS_DN_VALUE}/
|
171
|
+
rval.should =~ /#{TEST_BASE_DN}/
|
172
|
+
rval.should =~ /\bnil\b/
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
it "create sub-branches for messages that match valid attributeType OIDs" do
|
177
|
+
@schema.should_receive( :attribute_types ).twice.
|
178
|
+
and_return({ :cn => :a_value, :ou => :a_value })
|
179
|
+
|
180
|
+
rval = @branch.cn( 'rondori' )
|
181
|
+
rval.dn.should == "cn=rondori,#{TEST_HOSTS_DN}"
|
182
|
+
|
183
|
+
rval2 = rval.ou( 'Config' )
|
184
|
+
rval2.dn.should == "ou=Config,cn=rondori,#{TEST_HOSTS_DN}"
|
185
|
+
end
|
186
|
+
|
187
|
+
it "create sub-branches for messages with additional attribute pairs" do
|
188
|
+
@schema.should_receive( :attribute_types ).
|
189
|
+
and_return({ :cn => :a_value, :ou => :a_value, :l => :a_value })
|
190
|
+
|
191
|
+
rval = @branch.cn( 'rondori', :l => 'Portland' )
|
192
|
+
rval.dn.should == "cn=rondori+l=Portland,#{TEST_HOSTS_DN}"
|
193
|
+
|
194
|
+
rval2 = rval.ou( 'Config' )
|
195
|
+
rval2.dn.should == "ou=Config,cn=rondori+l=Portland,#{TEST_HOSTS_DN}"
|
196
|
+
end
|
197
|
+
|
198
|
+
it "don't create sub-branches for messages that don't match valid attributeType OIDs" do
|
199
|
+
@schema.should_receive( :attribute_types ).
|
200
|
+
and_return({ :cn => :a_value, :ou => :a_value })
|
201
|
+
|
202
|
+
lambda {
|
203
|
+
@branch.facelart( 'sbc' )
|
204
|
+
}.should raise_error( NoMethodError )
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
it "can return all of its immediate children as Branches" do
|
209
|
+
@directory.should_receive( :search ).
|
210
|
+
with( @branch, :one, '(objectClass=*)' ).
|
211
|
+
and_return([ :the_children ])
|
212
|
+
@branch.children.should == [ :the_children ]
|
213
|
+
end
|
214
|
+
|
215
|
+
it "can return its parent as a Branch" do
|
216
|
+
parent_branch = stub( "parent branch object" )
|
217
|
+
@branch.should_receive( :class ).and_return( Treequel::Branch )
|
218
|
+
Treequel::Branch.should_receive( :new ).with( @directory, TEST_BASE_DN ).
|
219
|
+
and_return( parent_branch )
|
220
|
+
@branch.parent.should == parent_branch
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
it "can construct a Treequel::Branchset that uses it as its base" do
|
225
|
+
branchset = stub( "branchset" )
|
226
|
+
Treequel::Branchset.should_receive( :new ).with( @branch ).
|
227
|
+
and_return( branchset )
|
228
|
+
|
229
|
+
@branch.branchset.should == branchset
|
230
|
+
end
|
231
|
+
|
232
|
+
it "can create a filtered Treequel::Branchset for itself" do
|
233
|
+
branchset = mock( "filtered branchset" )
|
234
|
+
Treequel::Branchset.should_receive( :new ).with( @branch ).
|
235
|
+
and_return( branchset )
|
236
|
+
branchset.should_receive( :filter ).with( {:cn => 'acme'} ).
|
237
|
+
and_return( :a_filtered_branchset )
|
238
|
+
|
239
|
+
@branch.filter( :cn => 'acme' ).should == :a_filtered_branchset
|
240
|
+
end
|
241
|
+
|
242
|
+
it "doesn't restrict the number of arguments passed to #filter (bugfix)" do
|
243
|
+
branchset = mock( "filtered branchset" )
|
244
|
+
Treequel::Branchset.should_receive( :new ).with( @branch ).
|
245
|
+
and_return( branchset )
|
246
|
+
branchset.should_receive( :filter ).with( :uid, [:glumpy, :grumpy, :glee] ).
|
247
|
+
and_return( :a_filtered_branchset )
|
248
|
+
|
249
|
+
@branch.filter( :uid, [:glumpy, :grumpy, :glee] ).should == :a_filtered_branchset
|
250
|
+
end
|
251
|
+
|
252
|
+
it "can create a scoped Treequel::Branchset for itself" do
|
253
|
+
branchset = mock( "scoped branchset" )
|
254
|
+
Treequel::Branchset.should_receive( :new ).with( @branch ).
|
255
|
+
and_return( branchset )
|
256
|
+
branchset.should_receive( :scope ).with( :onelevel ).
|
257
|
+
and_return( :a_scoped_branchset )
|
258
|
+
|
259
|
+
@branch.scope( :onelevel ).should == :a_scoped_branchset
|
260
|
+
end
|
261
|
+
|
262
|
+
it "can create a selective Treequel::Branchset for itself" do
|
263
|
+
branchset = mock( "selective branchset" )
|
264
|
+
Treequel::Branchset.should_receive( :new ).with( @branch ).
|
265
|
+
and_return( branchset )
|
266
|
+
branchset.should_receive( :select ).with( :uid, :l, :familyName, :givenName ).
|
267
|
+
and_return( :a_selective_branchset )
|
268
|
+
|
269
|
+
@branch.select( :uid, :l, :familyName, :givenName ).should == :a_selective_branchset
|
270
|
+
end
|
271
|
+
|
272
|
+
it "knows which objectClasses it has" do
|
273
|
+
oc_attr = mock( "objectClass attributeType object" )
|
274
|
+
@schema.should_receive( :attribute_types ).and_return({ :objectClass => oc_attr })
|
275
|
+
oc_attr.should_receive( :single? ).and_return( false )
|
276
|
+
oc_attr.should_receive( :syntax_oid ).and_return( OIDS::STRING_SYNTAX )
|
277
|
+
|
278
|
+
@entry.should_receive( :[] ).with( 'objectClass' ).at_least( :once ).
|
279
|
+
and_return([ 'organizationalUnit', 'extensibleObject' ])
|
280
|
+
|
281
|
+
@directory.should_receive( :convert_syntax_value ).
|
282
|
+
with( OIDS::STRING_SYNTAX, 'organizationalUnit' ).
|
283
|
+
and_return( 'organizationalUnit' )
|
284
|
+
@directory.should_receive( :convert_syntax_value ).
|
285
|
+
with( OIDS::STRING_SYNTAX, 'extensibleObject' ).
|
286
|
+
and_return( 'extensibleObject' )
|
287
|
+
|
288
|
+
@schema.should_receive( :object_classes ).twice.and_return({
|
289
|
+
:organizationalUnit => :ou_objectclass,
|
290
|
+
:extensibleObject => :extobj_objectclass,
|
291
|
+
})
|
292
|
+
|
293
|
+
@branch.object_classes.should == [ :ou_objectclass, :extobj_objectclass ]
|
294
|
+
end
|
295
|
+
|
296
|
+
it "can return the set of all its MUST attributeTypes based on which objectClasses it has" do
|
297
|
+
oc1 = mock( "first objectclass" )
|
298
|
+
oc2 = mock( "second objectclass" )
|
299
|
+
|
300
|
+
@branch.should_receive( :object_classes ).and_return([ oc1, oc2 ])
|
301
|
+
oc1.should_receive( :must ).at_least( :once ).and_return([ :cn, :uid ])
|
302
|
+
oc2.should_receive( :must ).at_least( :once ).and_return([ :cn, :l ])
|
303
|
+
|
304
|
+
must_attrs = @branch.must_attribute_types
|
305
|
+
must_attrs.should have( 3 ).members
|
306
|
+
must_attrs.should include( :cn, :uid, :l )
|
307
|
+
end
|
308
|
+
|
309
|
+
it "can return a Hash pre-populated with pairs that correspond to its MUST attributes" do
|
310
|
+
cn_attrtype = mock( "cn attribute type", :single? => true )
|
311
|
+
l_attrtype = mock( "l attribute type", :single? => true )
|
312
|
+
objectClass_attrtype = mock( "objectClass attribute type", :single? => false )
|
313
|
+
|
314
|
+
cn_attrtype.should_receive( :name ).at_least( :once ).and_return( :cn )
|
315
|
+
l_attrtype.should_receive( :name ).at_least( :once ).and_return( :l )
|
316
|
+
objectClass_attrtype.should_receive( :name ).at_least( :once ).and_return( :objectClass )
|
317
|
+
|
318
|
+
@branch.should_receive( :must_attribute_types ).at_least( :once ).
|
319
|
+
and_return([ cn_attrtype, l_attrtype, objectClass_attrtype ])
|
320
|
+
|
321
|
+
@branch.must_attributes_hash.
|
322
|
+
should == { :cn => '', :l => '', :objectClass => [:top] }
|
323
|
+
end
|
324
|
+
|
325
|
+
|
326
|
+
it "can return the set of all its MAY attributeTypes based on which objectClasses it has" do
|
327
|
+
oc1 = mock( "first objectclass" )
|
328
|
+
oc2 = mock( "second objectclass" )
|
329
|
+
|
330
|
+
@branch.should_receive( :object_classes ).and_return([ oc1, oc2 ])
|
331
|
+
oc1.should_receive( :may ).and_return([ :description, :mobilePhone ])
|
332
|
+
oc2.should_receive( :may ).and_return([ :chunktype ])
|
333
|
+
|
334
|
+
must_attrs = @branch.may_attribute_types
|
335
|
+
must_attrs.should have( 3 ).members
|
336
|
+
must_attrs.should include( :description, :mobilePhone, :chunktype )
|
337
|
+
end
|
338
|
+
|
339
|
+
it "can return the set of all of its valid attributeTypes, which is a union of its " +
|
340
|
+
"MUST and MAY attributes" do
|
341
|
+
@branch.should_receive( :must_attribute_types ).
|
342
|
+
and_return([ :cn, :l, :uid ])
|
343
|
+
@branch.should_receive( :may_attribute_types ).
|
344
|
+
and_return([ :description, :mobilePhone, :chunktype ])
|
345
|
+
|
346
|
+
all_attrs = @branch.valid_attribute_types
|
347
|
+
|
348
|
+
all_attrs.should have( 6 ).members
|
349
|
+
all_attrs.should include( :cn, :uid, :l, :description, :mobilePhone, :chunktype )
|
350
|
+
end
|
351
|
+
|
352
|
+
it "knows if a attribute is valid given its objectClasses" do
|
353
|
+
attrs = mock( "Attribute list", :null_object => true )
|
354
|
+
|
355
|
+
@branch.should_receive( :valid_attribute_types ).
|
356
|
+
twice.
|
357
|
+
and_return([ attrs ])
|
358
|
+
|
359
|
+
attrs.should_receive( :names ).twice.and_return([ :cn, :l, :uid ])
|
360
|
+
|
361
|
+
@branch.valid_attribute?( :uid ).should be_true()
|
362
|
+
@branch.valid_attribute?( :rubberChicken ).should be_false()
|
363
|
+
end
|
364
|
+
|
365
|
+
it "can be moved to a new location within the directory" do
|
366
|
+
newdn = "ou=hosts,dc=admin,#{TEST_BASE_DN}"
|
367
|
+
@directory.should_receive( :move ).with( @branch, newdn, {} )
|
368
|
+
@branch.move( newdn )
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
it "resets any cached data when its DN changes" do
|
373
|
+
@directory.should_receive( :get_entry ).with( @branch ).
|
374
|
+
and_return( :first_entry, :second_entry )
|
375
|
+
|
376
|
+
@branch.entry
|
377
|
+
@branch.dn = TEST_HOSTS_DN
|
378
|
+
@branch.entry.should == :second_entry
|
379
|
+
end
|
380
|
+
|
381
|
+
|
382
|
+
it "can be deleted from the directory" do
|
383
|
+
@directory.should_receive( :delete ).with( @branch )
|
384
|
+
@branch.delete
|
385
|
+
end
|
386
|
+
|
387
|
+
|
388
|
+
it "can create children under itself" do
|
389
|
+
newattrs = {
|
390
|
+
:ipHostNumber => '127.0.0.1',
|
391
|
+
:objectClass => [:ipHost],
|
392
|
+
}
|
393
|
+
@directory.should_receive( :create ).
|
394
|
+
with( an_instance_of(Treequel::Branch), newattrs ).
|
395
|
+
and_return( true )
|
396
|
+
|
397
|
+
@branch.cn( :chillyt ).create( newattrs )
|
398
|
+
end
|
399
|
+
|
400
|
+
|
401
|
+
it "can copy itself to a sibling entry" do
|
402
|
+
newbranch = stub( "copied sibling branch" )
|
403
|
+
Treequel::Branch.should_receive( :new ).with( @directory, TEST_PERSON2_DN ).
|
404
|
+
and_return( newbranch )
|
405
|
+
@entry.should_receive( :merge ).with( {} ).and_return( :merged_attributes )
|
406
|
+
@directory.should_receive( :create ).with( newbranch, :merged_attributes ).
|
407
|
+
and_return( true )
|
408
|
+
|
409
|
+
@branch.copy( TEST_PERSON2_DN ).should == newbranch
|
410
|
+
end
|
411
|
+
|
412
|
+
|
413
|
+
it "can copy itself to a sibling entry with attribute changes" do
|
414
|
+
oldattrs = { :sn => "Davies", :firstName => 'David' }
|
415
|
+
newattrs = { :sn => "Michaels", :firstName => 'George' }
|
416
|
+
newbranch = stub( "copied sibling branch" )
|
417
|
+
Treequel::Branch.should_receive( :new ).with( @directory, TEST_PERSON2_DN ).
|
418
|
+
and_return( newbranch )
|
419
|
+
@entry.should_receive( :merge ).with( newattrs ).and_return( newattrs )
|
420
|
+
@directory.should_receive( :create ).with( newbranch, newattrs ).
|
421
|
+
and_return( true )
|
422
|
+
|
423
|
+
@branch.copy( TEST_PERSON2_DN, newattrs ).should == newbranch
|
424
|
+
end
|
425
|
+
|
426
|
+
|
427
|
+
it "can modify its entry's attributes en masse by merging a Hash" do
|
428
|
+
attributes = {
|
429
|
+
:displayName => 'Chilly T. Penguin',
|
430
|
+
:description => "A chilly little penguin.",
|
431
|
+
}
|
432
|
+
|
433
|
+
@directory.should_receive( :modify ).with( @branch, attributes )
|
434
|
+
|
435
|
+
@branch.merge( attributes )
|
436
|
+
end
|
437
|
+
|
438
|
+
|
439
|
+
it "knows how to represent its DN as an RFC1781-style UFN" do
|
440
|
+
@branch.to_ufn.should =~ /Hosts, acme\.com/i
|
441
|
+
end
|
442
|
+
|
443
|
+
|
444
|
+
it "knows how to represent itself as LDIF" do
|
445
|
+
@entry.should_receive( :keys ).and_return([ 'description', 'l' ])
|
446
|
+
@entry.should_receive( :[] ).with( 'description' ).
|
447
|
+
and_return([ 'A chilly little penguin.' ])
|
448
|
+
@entry.should_receive( :[] ).with( 'l' ).
|
449
|
+
and_return([ 'Antartica', 'Galapagos' ])
|
450
|
+
|
451
|
+
ldif = @branch.to_ldif
|
452
|
+
ldif.should =~ /dn: #{TEST_HOSTS_DN_ATTR}=#{TEST_HOSTS_DN_VALUE},#{TEST_BASE_DN}/i
|
453
|
+
ldif.should =~ /description: A chilly little penguin./
|
454
|
+
end
|
455
|
+
|
456
|
+
|
457
|
+
it "returns a Treequel::BranchCollection with equivalent Branchsets if added to another " +
|
458
|
+
"Branch" do
|
459
|
+
other_branch = Treequel::Branch.new( @directory, TEST_SUBHOSTS_DN )
|
460
|
+
|
461
|
+
Treequel::Branchset.should_receive( :new ).with( @branch ).and_return( :branchset )
|
462
|
+
Treequel::Branchset.should_receive( :new ).with( other_branch ).and_return( :other_branchset )
|
463
|
+
Treequel::BranchCollection.should_receive( :new ).with( :branchset, :other_branchset ).
|
464
|
+
and_return( :a_collection )
|
465
|
+
|
466
|
+
(@branch + other_branch).should == :a_collection
|
467
|
+
end
|
468
|
+
|
469
|
+
|
470
|
+
### Attribute reader
|
471
|
+
describe "index fetch operator" do
|
472
|
+
|
473
|
+
it "fetches a multi-value attribute as an Array of Strings" do
|
474
|
+
@schema.should_receive( :attribute_types ).and_return({ :glumpy => @attribute_type })
|
475
|
+
@attribute_type.should_receive( :single? ).and_return( false )
|
476
|
+
@entry.should_receive( :[] ).with( 'glumpy' ).at_least( :once ).
|
477
|
+
and_return([ 'glumpa1', 'glumpa2' ])
|
478
|
+
|
479
|
+
@attribute_type.stub!( :syntax_oid ).and_return( OIDS::STRING_SYNTAX )
|
480
|
+
@directory.stub!( :convert_syntax_value ).and_return {|_,str| str }
|
481
|
+
|
482
|
+
@branch[ :glumpy ].should == [ 'glumpa1', 'glumpa2' ]
|
483
|
+
end
|
484
|
+
|
485
|
+
it "fetches a single-value attribute as a scalar String" do
|
486
|
+
@schema.should_receive( :attribute_types ).and_return({ :glumpy => @attribute_type })
|
487
|
+
@attribute_type.should_receive( :single? ).and_return( true )
|
488
|
+
@entry.should_receive( :[] ).with( 'glumpy' ).at_least( :once ).
|
489
|
+
and_return([ 'glumpa1' ])
|
490
|
+
|
491
|
+
@attribute_type.stub!( :syntax_oid ).and_return( OIDS::STRING_SYNTAX )
|
492
|
+
@directory.stub!( :convert_syntax_value ).and_return {|_,str| str }
|
493
|
+
|
494
|
+
@branch[ :glumpy ].should == 'glumpa1'
|
495
|
+
end
|
496
|
+
|
497
|
+
it "returns the entry without conversion if there is no such attribute in the schema" do
|
498
|
+
@schema.should_receive( :attribute_types ).and_return({})
|
499
|
+
@entry.should_receive( :[] ).with( 'glumpy' ).at_least( :once ).
|
500
|
+
and_return([ 'glumpa1' ])
|
501
|
+
@branch[ :glumpy ].should == [ 'glumpa1' ]
|
502
|
+
end
|
503
|
+
|
504
|
+
it "returns nil if record doesn't have the attribute set" do
|
505
|
+
@entry.should_receive( :[] ).with( 'glumpy' ).and_return( nil )
|
506
|
+
@branch[ :glumpy ].should == nil
|
507
|
+
end
|
508
|
+
|
509
|
+
it "caches the value fetched from its entry" do
|
510
|
+
@schema.stub!( :attribute_types ).and_return({ :glump => @attribute_type })
|
511
|
+
@attribute_type.stub!( :single? ).and_return( true )
|
512
|
+
@attribute_type.stub!( :syntax_oid ).and_return( OIDS::STRING_SYNTAX )
|
513
|
+
@directory.stub!( :convert_syntax_value ).and_return {|_,str| str }
|
514
|
+
@entry.should_receive( :[] ).with( 'glump' ).once.and_return( [:a_value] )
|
515
|
+
2.times { @branch[ :glump ] }
|
516
|
+
end
|
517
|
+
|
518
|
+
it "maps attributes through its directory" do
|
519
|
+
@schema.should_receive( :attribute_types ).and_return({ :bvector => @attribute_type })
|
520
|
+
@attribute_type.should_receive( :single? ).and_return( true )
|
521
|
+
@entry.should_receive( :[] ).with( 'bvector' ).at_least( :once ).
|
522
|
+
and_return([ '010011010101B' ])
|
523
|
+
@attribute_type.should_receive( :syntax_oid ).and_return( OIDS::BIT_STRING_SYNTAX )
|
524
|
+
@directory.should_receive( :convert_syntax_value ).
|
525
|
+
with( OIDS::BIT_STRING_SYNTAX, '010011010101B' ).
|
526
|
+
and_return( 1237 )
|
527
|
+
|
528
|
+
@branch[ :bvector ].should == 1237
|
529
|
+
end
|
530
|
+
|
531
|
+
end
|
532
|
+
|
533
|
+
### Attribute writer
|
534
|
+
describe "index set operator" do
|
535
|
+
|
536
|
+
it "writes a single value attribute via its directory" do
|
537
|
+
@directory.should_receive( :modify ).with( @branch, { 'glumpy' => ['gits'] } )
|
538
|
+
@entry.should_receive( :[]= ).with( 'glumpy', ['gits'] )
|
539
|
+
@branch[ :glumpy ] = 'gits'
|
540
|
+
end
|
541
|
+
|
542
|
+
it "writes multiple attribute values via its directory" do
|
543
|
+
@directory.should_receive( :modify ).with( @branch, { 'glumpy' => ['gits', 'crumps'] } )
|
544
|
+
@entry.should_receive( :[]= ).with( 'glumpy', ['gits', 'crumps'] )
|
545
|
+
@branch[ :glumpy ] = [ 'gits', 'crumps' ]
|
546
|
+
end
|
547
|
+
|
548
|
+
it "clears the cache after a successful write" do
|
549
|
+
@schema.stub!( :attribute_types ).and_return({ :glorpy => @attribute_type })
|
550
|
+
@attribute_type.stub!( :single? ).and_return( true )
|
551
|
+
@attribute_type.stub!( :syntax_oid ).and_return( OIDS::STRING_SYNTAX )
|
552
|
+
@directory.stub!( :convert_syntax_value ).and_return {|_,val| val }
|
553
|
+
@entry.should_receive( :[] ).with( 'glorpy' ).and_return( [:firstval], [:secondval] )
|
554
|
+
|
555
|
+
@directory.should_receive( :modify ).with( @branch, {'glorpy' => ['chunks']} )
|
556
|
+
@entry.should_receive( :[]= ).with( 'glorpy', ['chunks'] )
|
557
|
+
|
558
|
+
@branch[ :glorpy ].should == :firstval
|
559
|
+
@branch[ :glorpy ] = 'chunks'
|
560
|
+
@branch[ :glorpy ].should == :secondval
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
end
|
565
|
+
|
566
|
+
end
|
567
|
+
|
568
|
+
|
569
|
+
# vim: set nosta noet ts=4 sw=4:
|