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.
Files changed (74) hide show
  1. data/ChangeLog +354 -0
  2. data/LICENSE +27 -0
  3. data/README +66 -0
  4. data/Rakefile +345 -0
  5. data/Rakefile.local +43 -0
  6. data/bin/treeirb +14 -0
  7. data/bin/treequel +229 -0
  8. data/examples/company-directory.rb +112 -0
  9. data/examples/ldap-monitor.rb +143 -0
  10. data/examples/ldap-monitor/public/css/master.css +328 -0
  11. data/examples/ldap-monitor/public/images/card_small.png +0 -0
  12. data/examples/ldap-monitor/public/images/chain_small.png +0 -0
  13. data/examples/ldap-monitor/public/images/globe_small.png +0 -0
  14. data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
  15. data/examples/ldap-monitor/public/images/plug.png +0 -0
  16. data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
  17. data/examples/ldap-monitor/public/images/tick.png +0 -0
  18. data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
  19. data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
  20. data/examples/ldap-monitor/views/backends.erb +41 -0
  21. data/examples/ldap-monitor/views/connections.erb +74 -0
  22. data/examples/ldap-monitor/views/databases.erb +39 -0
  23. data/examples/ldap-monitor/views/dump_subsystem.erb +14 -0
  24. data/examples/ldap-monitor/views/index.erb +14 -0
  25. data/examples/ldap-monitor/views/layout.erb +35 -0
  26. data/examples/ldap-monitor/views/listeners.erb +30 -0
  27. data/examples/ldap_state.rb +62 -0
  28. data/lib/treequel.rb +145 -0
  29. data/lib/treequel/branch.rb +589 -0
  30. data/lib/treequel/branchcollection.rb +204 -0
  31. data/lib/treequel/branchset.rb +360 -0
  32. data/lib/treequel/constants.rb +604 -0
  33. data/lib/treequel/directory.rb +541 -0
  34. data/lib/treequel/exceptions.rb +32 -0
  35. data/lib/treequel/filter.rb +704 -0
  36. data/lib/treequel/mixins.rb +325 -0
  37. data/lib/treequel/schema.rb +245 -0
  38. data/lib/treequel/schema/attributetype.rb +252 -0
  39. data/lib/treequel/schema/ldapsyntax.rb +96 -0
  40. data/lib/treequel/schema/matchingrule.rb +124 -0
  41. data/lib/treequel/schema/matchingruleuse.rb +124 -0
  42. data/lib/treequel/schema/objectclass.rb +289 -0
  43. data/lib/treequel/sequel_integration.rb +26 -0
  44. data/lib/treequel/utils.rb +169 -0
  45. data/rake/191_compat.rb +26 -0
  46. data/rake/dependencies.rb +76 -0
  47. data/rake/helpers.rb +434 -0
  48. data/rake/hg.rb +261 -0
  49. data/rake/manual.rb +782 -0
  50. data/rake/packaging.rb +135 -0
  51. data/rake/publishing.rb +318 -0
  52. data/rake/rdoc.rb +30 -0
  53. data/rake/style.rb +62 -0
  54. data/rake/svn.rb +668 -0
  55. data/rake/testing.rb +187 -0
  56. data/rake/verifytask.rb +64 -0
  57. data/rake/win32.rb +190 -0
  58. data/spec/lib/constants.rb +93 -0
  59. data/spec/lib/helpers.rb +100 -0
  60. data/spec/treequel/branch_spec.rb +569 -0
  61. data/spec/treequel/branchcollection_spec.rb +213 -0
  62. data/spec/treequel/branchset_spec.rb +376 -0
  63. data/spec/treequel/directory_spec.rb +487 -0
  64. data/spec/treequel/filter_spec.rb +482 -0
  65. data/spec/treequel/mixins_spec.rb +330 -0
  66. data/spec/treequel/schema/attributetype_spec.rb +237 -0
  67. data/spec/treequel/schema/ldapsyntax_spec.rb +83 -0
  68. data/spec/treequel/schema/matchingrule_spec.rb +158 -0
  69. data/spec/treequel/schema/matchingruleuse_spec.rb +137 -0
  70. data/spec/treequel/schema/objectclass_spec.rb +262 -0
  71. data/spec/treequel/schema_spec.rb +118 -0
  72. data/spec/treequel/utils_spec.rb +49 -0
  73. data/spec/treequel_spec.rb +179 -0
  74. metadata +169 -0
@@ -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: