treequel 1.4.4 → 1.5.0pre445

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.
@@ -70,6 +70,7 @@ class Treequel::Model < Treequel::Branch
70
70
 
71
71
  @objectclass_registry = SET_HASH.dup
72
72
  @base_registry = SET_HASH.dup
73
+ @directory = nil
73
74
 
74
75
  class << self
75
76
  attr_reader :objectclass_registry
@@ -77,6 +78,25 @@ class Treequel::Model < Treequel::Branch
77
78
  end
78
79
 
79
80
 
81
+ ### Return the Treequel::Directory the Model will use for searches, creating it if it
82
+ ### hasn't been created already. The default Directory will be created by calling
83
+ ### Treequel.directory_from_config.
84
+ ### @return [Treequel::Directory] the default directory
85
+ def self::directory
86
+ self.directory = Treequel.directory_from_config unless @directory
87
+ return @directory
88
+ end
89
+
90
+
91
+ ### Set the Treequel::Directory that should be used for searches. The receiving class will also
92
+ ### be set as the #results_class of the +newdirectory+.
93
+ ### @param [Treequel::Directory] newdirectory
94
+ def self::directory=( newdirectory )
95
+ @directory = newdirectory
96
+ @directory.results_class = self if @directory
97
+ end
98
+
99
+
80
100
  ### Inheritance callback -- add a class-specific objectclass registry to inheriting classes.
81
101
  ### @param [Class] subclass the inheriting class
82
102
  def self::inherited( subclass )
@@ -10,6 +10,7 @@ require 'treequel/constants'
10
10
  # Mixin that provides Treequel::Model characteristics to a mixin module.
11
11
  module Treequel::Model::ObjectClass
12
12
  include Treequel::HashUtilities
13
+ extend Treequel::Delegation
13
14
 
14
15
 
15
16
  ### Extension callback -- add data structures to the extending +mod+.
@@ -30,6 +31,17 @@ module Treequel::Model::ObjectClass
30
31
  end
31
32
 
32
33
 
34
+ #################################################################
35
+ ### I N S T A N C E M E T H O D S
36
+ #################################################################
37
+
38
+ # Delegate Branchset methods through #search to allow ObjectClass.filter as a shortcut for
39
+ # ObjectClass.search.filter
40
+ def_method_delegators :search,
41
+ :collection, :map, :to_hash, :each, :first,
42
+ :filter, :scope, :select, :limit, :timeout, :as, :from
43
+
44
+
33
45
  ### Declare which Treequel::Model subclasses the mixin will register itself with. If this is
34
46
  ### used, it should be declared *before* declaring the mixin's bases and/or objectClasses.
35
47
  def model_class( mclass=nil )
@@ -75,11 +87,30 @@ module Treequel::Model::ObjectClass
75
87
  end
76
88
 
77
89
 
78
- ### Instantiate a new Treequel::Model object with given +dn+ and the objectclasses
79
- ### specified by the receiving module.
80
- ### @param [#to_s] dn the DN of the new model object
81
- ### @param [Hash] entryhash attributes to set on the new entry
82
- def create( directory, dn, entryhash={} )
90
+ ### @overload create( dn, entryhash={} )
91
+ ### Create a new instance of the mixin's model_class in the model_class's default
92
+ ### directory with the given +dn+ and the objectclasses specified by the mixin. If the
93
+ ### optional +entryhash+ is given, it will be used as the initial attributes of the
94
+ ### new entry.
95
+ ### @param [#to_s] dn the DN of the new model object
96
+ ### @param [Hash] entryhash attributes to set on the new entry
97
+ ### @overload create( directory, dn, entryhash={} )
98
+ ### Create a new instance of the mixin's model_class in the specified +directory+
99
+ ### with the given +dn+ and the objectclasses specified by the mixin. If the
100
+ ### optional +entryhash+ is given, it will be used as the initial attributes of the
101
+ ### new entry.
102
+ ### @param [Treequel::Directory] directory the directory to create the entry in (optional)
103
+ ### @param [#to_s] dn the DN of the new model object
104
+ ### @param [Hash] entryhash attributes to set on the new entry
105
+ def create( directory, dn=nil, entryhash={} )
106
+
107
+ # Shift the arguments if the first one isn't a directory
108
+ unless directory.is_a?( Treequel::Directory )
109
+ entryhash = dn || {}
110
+ dn = directory
111
+ directory = self.model_class.directory
112
+ end
113
+
83
114
  entryhash = stringify_keys( entryhash )
84
115
 
85
116
  # Add the objectclasses from the mixin
@@ -103,17 +134,20 @@ module Treequel::Model::ObjectClass
103
134
  ### base) that can be used to search the given +directory+ for entries to which
104
135
  ### the receiver applies.
105
136
  ###
106
- ### @param [Treequel::Directory] directory the directory to search
137
+ ### @param [Treequel::Directory] directory the directory to search; if not given, this defaults
138
+ ### to the directory associated with the module's
139
+ ### model_class.
107
140
  ### @return [Treequel::Branchset, Treequel::BranchCollection] the encapsulated search
108
- def search( directory )
141
+ def search( directory=nil )
142
+ directory ||= self.model_class.directory
109
143
  bases = self.model_bases
110
144
  objectclasses = self.model_objectclasses
111
145
 
112
146
  raise Treequel::ModelError, "%p has no search criteria defined" % [ self ] if
113
147
  bases.empty? && objectclasses.empty?
114
148
 
115
- Treequel.log.debug "Creating search for %s using model class %p" %
116
- [ self.name, self.model_class ]
149
+ Treequel.log.debug "Creating search for %p using model class %p" %
150
+ [ self, self.model_class ]
117
151
 
118
152
  # Start by making a Branchset or BranchCollection for the mixin's bases. If
119
153
  # the mixin doesn't have any bases, just use the base DN of the directory
@@ -87,7 +87,7 @@ module Treequel::TestConstants # :nodoc:all
87
87
  TEST_PEOPLE_DN = "#{TEST_PEOPLE_RDN},#{TEST_BASE_DN}"
88
88
 
89
89
  TEST_PERSON_DN_ATTR = 'uid'
90
- TEST_PERSON_DN_VALUE = 'jrandom'
90
+ TEST_PERSON_DN_VALUE = 'slappy'
91
91
  TEST_PERSON_RDN = "#{TEST_PERSON_DN_ATTR}=#{TEST_PERSON_DN_VALUE}"
92
92
  TEST_PERSON_DN = "#{TEST_PERSON_RDN},#{TEST_PEOPLE_DN}"
93
93
 
@@ -124,6 +124,67 @@ module Treequel::TestConstants # :nodoc:all
124
124
  ]
125
125
  TEST_HOST_MULTIVALUE_DN = "#{TEST_HOST_MULTIVALUE_RDN},#{TEST_HOSTS_DN}"
126
126
 
127
+ # Test entry hashes
128
+ TEST_HOSTS_ENTRY = {
129
+ 'dn' => [TEST_HOSTS_DN],
130
+ TEST_HOSTS_DN_ATTR => [TEST_HOSTS_DN_VALUE],
131
+ 'objectClass' => ['top', 'organizationalUnit'],
132
+ 'description' => ['Hosts under acme.com'],
133
+ }
134
+
135
+ TEST_PEOPLE_ENTRY = {
136
+ 'dn' => [TEST_PEOPLE_DN],
137
+ TEST_PEOPLE_DN_ATTR => [TEST_PEOPLE_DN_VALUE],
138
+ 'objectClass' => ['top', 'organizationalUnit'],
139
+ 'description' => ['Acme.com employees'],
140
+ }
141
+
142
+ TEST_PERSON_ENTRY = {
143
+ 'dn' => [TEST_PERSON_DN],
144
+ TEST_PERSON_DN_ATTR => [TEST_PERSON_DN_VALUE],
145
+ 'cn' => ['Slappy the Frog'],
146
+ 'givenName' => ['Slappy'],
147
+ 'sn' => ['Frog'],
148
+ 'l' => ['a forest in England'],
149
+ 'title' => ['Forest Fire Prevention Advocate'],
150
+ 'displayName' => ['Slappy the Frog'],
151
+ 'logonTime' => ['1293167318'],
152
+ 'uidNumber' => ['1121'],
153
+ 'gidNumber' => ['200'],
154
+ 'homeDirectory' => ['/u/j/jrandom'],
155
+ 'description' => [
156
+ 'Smokey the Bear is much more intense in person.',
157
+ 'Alright.'
158
+ ],
159
+ 'objectClass' => %w[
160
+ top
161
+ person
162
+ organizationalPerson
163
+ inetOrgPerson
164
+ posixAccount
165
+ shadowAccount
166
+ apple-user
167
+ ],
168
+ }
169
+
170
+ TEST_OPERATIONAL_PEOPLE_ENTRY = {
171
+ TEST_PEOPLE_DN_ATTR => [TEST_PEOPLE_DN_VALUE],
172
+ 'structuralObjectClass' => ['organizationalUnit'],
173
+ 'entryUUID' => ['5035e674-bae3-102b-992e-e9e937d524d6'],
174
+ 'creatorsName' => ['cn=admin,dc=laika,dc=com'],
175
+ 'createTimestamp' => ['20070629232213Z'],
176
+ 'entryCSN' => ['20070629232213.000000Z#000000#000#000000'],
177
+ 'modifiersName' => ['cn=admin,dc=laika,dc=com'],
178
+ 'modifyTimestamp' => ['20070629232213Z'],
179
+ 'entryDN' => [TEST_PEOPLE_DN],
180
+ 'subschemaSubentry' => ['cn=Subschema'],
181
+ 'hasSubordinates' => ['TRUE'],
182
+ 'dn' => [TEST_PEOPLE_DN],
183
+ 'objectClass' => ['top', 'organizationalUnit'],
184
+ 'description' => ['Acme.com employees'],
185
+ }
186
+
187
+
127
188
  constants.each do |cname|
128
189
  const_get(cname).freeze
129
190
  end
@@ -127,7 +127,7 @@ module Treequel::SpecHelpers
127
127
  ### LDAP connection. Also pre-loads the schema object and fixtures some other
128
128
  ### external data.
129
129
  def get_fixtured_directory( conn )
130
- LDAP::SSLConn.stub( :new ).and_return( @conn )
130
+ LDAP::SSLConn.stub( :new ).and_return( conn )
131
131
  conn.stub( :search_ext2 ).
132
132
  with( "", 0, "(objectClass=*)", ["+"], false, nil, nil, 0, 0, 0, "", nil ).
133
133
  and_return( TEST_DSE )
@@ -163,6 +163,8 @@ end
163
163
 
164
164
  ### Mock with Rspec
165
165
  Rspec.configure do |c|
166
+ include Treequel::TestConstants
167
+
166
168
  c.mock_with :rspec
167
169
 
168
170
  c.extend( Treequel::TestConstants )
@@ -28,9 +28,6 @@ include Treequel::Constants
28
28
  #####################################################################
29
29
 
30
30
  describe Treequel::Branch do
31
- include Treequel::SpecHelpers,
32
- Treequel::Matchers
33
-
34
31
 
35
32
  before( :all ) do
36
33
  setup_logging( :fatal )
@@ -241,13 +238,13 @@ describe Treequel::Branch do
241
238
 
242
239
  it "clears any cached values if its include_operational_attrs attribute is changed" do
243
240
  @directory.should_receive( :get_entry ).with( @branch ).exactly( :once ).
244
- and_return( :the_entry )
241
+ and_return( TEST_PEOPLE_ENTRY.dup )
245
242
  @directory.should_receive( :get_extended_entry ).with( @branch ).exactly( :once ).
246
- and_return( :the_extended_entry )
243
+ and_return( TEST_OPERATIONAL_PEOPLE_ENTRY.dup )
247
244
 
248
- @branch.entry.should == :the_entry
245
+ @branch.entry.should == TEST_PEOPLE_ENTRY.dup.tap {|entry| entry.delete('dn') }
249
246
  @branch.include_operational_attrs = true
250
- @branch.entry.should == :the_extended_entry
247
+ @branch.entry.should == TEST_OPERATIONAL_PEOPLE_ENTRY.dup.tap {|entry| entry.delete('dn') }
251
248
  end
252
249
 
253
250
  it "returns a human-readable representation of itself for #inspect" do
@@ -34,6 +34,7 @@ describe Treequel::Branchset do
34
34
  :server_controls => [],
35
35
  }
36
36
 
37
+
37
38
  before( :all ) do
38
39
  setup_logging( :fatal )
39
40
  end
@@ -56,12 +57,10 @@ describe Treequel::Branchset do
56
57
  end
57
58
 
58
59
  it "is Enumerable" do
59
- resultbranch = mock( "Result Branch" )
60
-
61
- @branch.should_receive( :search ).
62
- with( Treequel::Branchset::DEFAULT_SCOPE, @branchset.filter, @params ).
63
- and_yield( resultbranch )
64
- resultbranch.should_receive( :dn ).and_return( :its_dn )
60
+ @conn.should_receive( :search_ext2 ).
61
+ with( TEST_BASE_DN, LDAP::LDAP_SCOPE_SUBTREE, "(objectClass=*)",
62
+ [], false, [], [], 0, 0, 0, "", nil ).
63
+ and_return([ TEST_HOSTS_ENTRY.dup ])
65
64
 
66
65
  @branchset.all? {|b| b.dn }
67
66
  end
@@ -70,20 +69,18 @@ describe Treequel::Branchset do
70
69
  # #empty?
71
70
  #
72
71
  it "is empty if it doesn't match at least one entry" do
73
- params = @params.merge( :limit => 1 )
74
- @branch.should_receive( :search ).
75
- with( Treequel::Branchset::DEFAULT_SCOPE, @branchset.filter, params ).
76
- and_return( [] )
77
-
72
+ @conn.should_receive( :search_ext2 ).
73
+ with( TEST_BASE_DN, LDAP::LDAP_SCOPE_SUBTREE, "(objectClass=*)",
74
+ [], false, [], [], 0, 0, 1, "", nil ).
75
+ and_return([ ])
78
76
  @branchset.should be_empty()
79
77
  end
80
78
 
81
79
  it "isn't empty if it matches at least one entry" do
82
- params = @params.merge( :limit => 1 )
83
- @branch.should_receive( :search ).
84
- with( Treequel::Branchset::DEFAULT_SCOPE, @branchset.filter, params ).
85
- and_return( [:a_branch] )
86
-
80
+ @conn.should_receive( :search_ext2 ).
81
+ with( TEST_BASE_DN, LDAP::LDAP_SCOPE_SUBTREE, "(objectClass=*)",
82
+ [], false, [], [], 0, 0, 1, "", nil ).
83
+ and_return([ TEST_HOSTS_ENTRY.dup ])
87
84
  @branchset.should_not be_empty()
88
85
  end
89
86
 
@@ -91,16 +88,12 @@ describe Treequel::Branchset do
91
88
  # #map
92
89
  #
93
90
  it "can be mapped into an Array of attribute values" do
94
- resultbranch = mock( "Result Branch" )
95
- resultbranch2 = mock( "Result Branch 2" )
91
+ @conn.should_receive( :search_ext2 ).
92
+ with( TEST_BASE_DN, LDAP::LDAP_SCOPE_SUBTREE, "(objectClass=*)",
93
+ [], false, [], [], 0, 0, 0, "", nil ).
94
+ and_return([ TEST_HOSTS_ENTRY.dup, TEST_PEOPLE_ENTRY.dup ])
96
95
 
97
- @branch.should_receive( :search ).
98
- with( Treequel::Branchset::DEFAULT_SCOPE, @branchset.filter, @params ).
99
- and_yield( resultbranch ).and_yield( resultbranch2 )
100
- resultbranch.should_receive( :[] ).with( :cn ).and_return([ :first_cn ])
101
- resultbranch2.should_receive( :[] ).with( :cn ).and_return([ :second_cn ])
102
-
103
- @branchset.map( :cn ).should == [[:first_cn], [:second_cn]]
96
+ @branchset.map( :ou ).should == [ ['Hosts'], ['People'] ]
104
97
  end
105
98
 
106
99
 
@@ -108,47 +101,30 @@ describe Treequel::Branchset do
108
101
  # #to_hash
109
102
  #
110
103
  it "can be mapped into a Hash of entries keyed by one of its attributes" do
111
- resultbranch = mock( "Result Branch" )
112
- resultbranch2 = mock( "Result Branch 2" )
113
-
114
- @branch.should_receive( :search ).
115
- with( Treequel::Branchset::DEFAULT_SCOPE, @branchset.filter, @params ).
116
- and_yield( resultbranch ).and_yield( resultbranch2 )
117
-
118
- resultbranch.should_receive( :[] ).with( :email ).
119
- and_return([ :first_email ])
120
- resultbranch.should_receive( :entry ).and_return( :entry1 )
121
- resultbranch2.should_receive( :[] ).with( :email ).
122
- and_return([ :second_email, :second_second_email ])
123
- resultbranch2.should_receive( :entry ).and_return( :entry2 )
124
-
125
- @branchset.to_hash( :email ).should == {
126
- :first_email => :entry1,
127
- :second_email => :entry2,
104
+ @conn.should_receive( :search_ext2 ).
105
+ with( "dc=acme,dc=com", 2, "(objectClass=*)", [], false, [], [], 0, 0, 0, "", nil ).
106
+ and_return([ TEST_HOSTS_ENTRY.dup, TEST_PEOPLE_ENTRY.dup ])
107
+
108
+ hosthash = TEST_HOSTS_ENTRY.dup
109
+ hosthash.delete( 'dn' )
110
+ peoplehash = TEST_PEOPLE_ENTRY.dup
111
+ peoplehash.delete( 'dn' )
112
+
113
+ @branchset.to_hash( :ou ).should == {
114
+ 'Hosts' => hosthash,
115
+ 'People' => peoplehash,
128
116
  }
129
117
  end
130
118
 
131
119
 
132
120
  it "can be mapped into a Hash of tuples using two attributes" do
133
- resultbranch = mock( "Result Branch" )
134
- resultbranch2 = mock( "Result Branch 2" )
121
+ @conn.should_receive( :search_ext2 ).
122
+ with( "dc=acme,dc=com", 2, "(objectClass=*)", [], false, [], [], 0, 0, 0, "", nil ).
123
+ and_return([ TEST_HOSTS_ENTRY.dup, TEST_PEOPLE_ENTRY.dup ])
135
124
 
136
- @branch.should_receive( :search ).
137
- with( Treequel::Branchset::DEFAULT_SCOPE, @branchset.filter, @params ).
138
- and_yield( resultbranch ).and_yield( resultbranch2 )
139
-
140
- resultbranch.should_receive( :[] ).with( :email ).
141
- and_return([ :first_email ])
142
- resultbranch.should_receive( :[] ).with( :cn ).
143
- and_return([ :first_cn ])
144
- resultbranch2.should_receive( :[] ).with( :email ).
145
- and_return([ :second_email, :second_second_email ])
146
- resultbranch2.should_receive( :[] ).with( :cn ).
147
- and_return([ :second_cn, :second_second_cn ])
148
-
149
- @branchset.to_hash( :email, :cn ).should == {
150
- :first_email => :first_cn,
151
- :second_email => :second_cn,
125
+ @branchset.to_hash( :ou, :description ).should == {
126
+ 'Hosts' => TEST_HOSTS_ENTRY['description'].first,
127
+ 'People' => TEST_PEOPLE_ENTRY['description'].first,
152
128
  }
153
129
  end
154
130
 
@@ -156,8 +132,7 @@ describe Treequel::Branchset do
156
132
  # #+
157
133
  #
158
134
  it "can be combined with another instance into a BranchCollection by adding them together" do
159
- other_branch = mock( "second treequel branch", :dn => 'theotherdn' )
160
- other_branch.stub( :directory ).and_return( @directory )
135
+ other_branch = @directory.ou( :people )
161
136
  other_branchset = Treequel::Branchset.new( other_branch )
162
137
 
163
138
  result = @branchset + other_branchset
@@ -167,18 +142,15 @@ describe Treequel::Branchset do
167
142
  end
168
143
 
169
144
  it "returns the results of the search with the additional Branch if one is added to it" do
170
- other_branch = mock( "additional treequel branch", :dn => 'theotherdn' )
171
- other_branch.stub( :to_ary ).and_return( [other_branch] )
172
- resultbranch = mock( "Result Branch" )
173
- resultbranch2 = mock( "Result Branch 2" )
145
+ @conn.should_receive( :search_ext2 ).
146
+ with( "dc=acme,dc=com", 2, "(objectClass=*)", [], false, [], [], 0, 0, 0, "", nil ).
147
+ and_return([ TEST_HOSTS_ENTRY.dup, TEST_PEOPLE_ENTRY.dup ])
174
148
 
175
- @branch.should_receive( :search ).
176
- with( Treequel::Branchset::DEFAULT_SCOPE, @branchset.filter, @params ).
177
- and_yield( resultbranch ).and_yield( resultbranch2 )
149
+ other_branch = @directory.ou( :netgroups )
178
150
 
179
151
  result = @branchset + other_branch
180
152
  result.should have( 3 ).members
181
- result.should include( other_branch, resultbranch, resultbranch2 )
153
+ result.should include( other_branch )
182
154
  end
183
155
 
184
156
  #
@@ -186,18 +158,15 @@ describe Treequel::Branchset do
186
158
  #
187
159
  it "returns the results of the search without the specified object if an object is " +
188
160
  "subtracted from it" do
189
- resultbranch = stub( "Result Branch", :dn => TEST_PERSON_DN )
190
- resultbranch2 = stub( "Result Branch 2", :dn => TEST_PERSON2_DN )
191
-
192
- otherbranch = stub( "Subtracted Branch", :dn => TEST_PERSON2_DN )
161
+ otherbranch = @directory.ou( :people )
193
162
 
194
- @branch.should_receive( :search ).
195
- with( Treequel::Branchset::DEFAULT_SCOPE, @branchset.filter, @params ).
196
- and_yield( resultbranch ).and_yield( resultbranch2 )
163
+ @conn.should_receive( :search_ext2 ).
164
+ with( "dc=acme,dc=com", 2, "(objectClass=*)", [], false, [], [], 0, 0, 0, "", nil ).
165
+ and_return([ TEST_HOSTS_ENTRY.dup, TEST_PEOPLE_ENTRY.dup ])
197
166
 
198
167
  result = @branchset - otherbranch
199
168
  result.should have( 1 ).members
200
- result.should_not include( resultbranch2 )
169
+ result.should_not include( otherbranch )
201
170
  end
202
171
 
203
172
  end
@@ -245,6 +245,14 @@ describe Treequel, "mixin" do
245
245
  @obj.delegated_method( :arg1, :arg2 )
246
246
  end
247
247
 
248
+ it "allows delegation to the delegate object's method with a block" do
249
+ @subobj.should_receive( :delegated_method ).with( :arg1 ).
250
+ and_yield( :the_block_argument )
251
+ blockarg = nil
252
+ @obj.delegated_method( :arg1 ) {|arg| blockarg = arg }
253
+ blockarg.should == :the_block_argument
254
+ end
255
+
248
256
  it "reports errors from its caller's perspective", :ruby_1_8_only => true do
249
257
  begin
250
258
  @obj.erroring_delegated_method
@@ -291,6 +299,14 @@ describe Treequel, "mixin" do
291
299
  @obj.delegated_method( :arg1, :arg2 )
292
300
  end
293
301
 
302
+ it "allows delegation to the delegate's method with a block" do
303
+ @subobj.should_receive( :delegated_method ).with( :arg1 ).
304
+ and_yield( :the_block_argument )
305
+ blockarg = nil
306
+ @obj.delegated_method( :arg1 ) {|arg| blockarg = arg }
307
+ blockarg.should == :the_block_argument
308
+ end
309
+
294
310
  it "reports errors from its caller's perspective", :ruby_1_8_only => true do
295
311
  begin
296
312
  @obj.erroring_delegated_method