treequel 1.0.0

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