treequel 1.4.4 → 1.5.0pre445

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