Skip to content

Commit b32f4e7

Browse files
committed
Merge pull request #235 from ruby-ldap/lazy-socket-initialization
Lazy initialize Net::LDAP::Connection's internal socket
2 parents 987c522 + 1aab8c9 commit b32f4e7

File tree

5 files changed

+139
-57
lines changed

5 files changed

+139
-57
lines changed

Diff for: lib/net/ldap.rb

+10-1
Original file line numberDiff line numberDiff line change
@@ -1275,6 +1275,11 @@ def inspect
12751275
inspected
12761276
end
12771277

1278+
# Internal: Set @open_connection for testing
1279+
def connection=(connection)
1280+
@open_connection = connection
1281+
end
1282+
12781283
private
12791284

12801285
# Yields an open connection if there is one, otherwise establishes a new
@@ -1300,13 +1305,17 @@ def use_connection(args)
13001305

13011306
# Establish a new connection to the LDAP server
13021307
def new_connection
1303-
Net::LDAP::Connection.new \
1308+
connection = Net::LDAP::Connection.new \
13041309
:host => @host,
13051310
:port => @port,
13061311
:hosts => @hosts,
13071312
:encryption => @encryption,
13081313
:instrumentation_service => @instrumentation_service,
13091314
:connect_timeout => @connect_timeout
1315+
1316+
# Force connect to see if there's a connection error
1317+
connection.socket
1318+
connection
13101319
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Net::LDAP::ConnectionRefusedError => e
13111320
@result = {
13121321
:resultCode => 52,

Diff for: lib/net/ldap/connection.rb

+48-10
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,28 @@ class Net::LDAP::Connection #:nodoc:
99
LdapVersion = 3
1010
MaxSaslChallenges = 10
1111

12-
def initialize(server)
12+
# Initialize a connection to an LDAP server
13+
#
14+
# :server
15+
# :hosts Array of tuples specifying host, port
16+
# :host host
17+
# :port port
18+
# :socket prepared socket
19+
#
20+
def initialize(server = {})
21+
@server = server
1322
@instrumentation_service = server[:instrumentation_service]
1423

15-
if server[:socket]
16-
prepare_socket(server)
17-
else
18-
server[:hosts] = [[server[:host], server[:port]]] if server[:hosts].nil?
19-
open_connection(server)
20-
end
24+
# Allows tests to parameterize what socket class to use
25+
@socket_class = server.fetch(:socket_class, DefaultSocket)
2126

2227
yield self if block_given?
2328
end
2429

30+
def socket_class=(socket_class)
31+
@socket_class = socket_class
32+
end
33+
2534
def prepare_socket(server)
2635
socket = server[:socket]
2736
encryption = server[:encryption]
@@ -41,7 +50,7 @@ def open_connection(server)
4150
errors = []
4251
hosts.each do |host, port|
4352
begin
44-
prepare_socket(server.merge(socket: Socket.tcp(host, port, socket_opts)))
53+
prepare_socket(server.merge(socket: @socket_class.new(host, port, socket_opts)))
4554
return
4655
rescue Net::LDAP::Error, SocketError, SystemCallError,
4756
OpenSSL::SSL::SSLError => e
@@ -202,7 +211,7 @@ def message_queue
202211
def read(syntax = Net::LDAP::AsnSyntax)
203212
ber_object =
204213
instrument "read.net_ldap_connection", :syntax => syntax do |payload|
205-
@conn.read_ber(syntax) do |id, content_length|
214+
socket.read_ber(syntax) do |id, content_length|
206215
payload[:object_type_id] = id
207216
payload[:content_length] = content_length
208217
end
@@ -232,7 +241,7 @@ def read(syntax = Net::LDAP::AsnSyntax)
232241
def write(request, controls = nil, message_id = next_msgid)
233242
instrument "write.net_ldap_connection" do |payload|
234243
packet = [message_id.to_ber, request, controls].compact.to_ber_sequence
235-
payload[:content_length] = @conn.write(packet)
244+
payload[:content_length] = socket.write(packet)
236245
end
237246
end
238247
private :write
@@ -652,4 +661,33 @@ def delete(args)
652661

653662
pdu
654663
end
664+
665+
# Internal: Returns a Socket like object used internally to communicate with
666+
# LDAP server.
667+
#
668+
# Typically a TCPSocket, but can be a OpenSSL::SSL::SSLSocket
669+
def socket
670+
return @conn if defined? @conn
671+
672+
# First refactoring uses the existing methods open_connection and
673+
# prepare_socket to set @conn. Next cleanup would centralize connection
674+
# handling here.
675+
if @server[:socket]
676+
prepare_socket(@server)
677+
else
678+
@server[:hosts] = [[@server[:host], @server[:port]]] if @server[:hosts].nil?
679+
open_connection(@server)
680+
end
681+
682+
@conn
683+
end
684+
685+
private
686+
687+
# Wrap around Socket.tcp to normalize with other Socket initializers
688+
class DefaultSocket
689+
def self.new(host, port, socket_opts = {})
690+
Socket.tcp(host, port, socket_opts)
691+
end
692+
end
655693
end # class Connection

Diff for: test/test_auth_adapter.rb

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
require 'test_helper'
22

33
class TestAuthAdapter < Test::Unit::TestCase
4-
def test_undefined_auth_adapter
5-
flexmock(Socket).should_receive(:tcp).ordered.with('ldap.example.com', 379, { connect_timeout: 5 }).once.and_return(nil)
4+
class FakeSocket
5+
def initialize(*args)
6+
end
7+
end
68

7-
conn = Net::LDAP::Connection.new(host: 'ldap.example.com', port: 379)
9+
def test_undefined_auth_adapter
10+
conn = Net::LDAP::Connection.new(host: 'ldap.example.com', port: 379, :socket_class => FakeSocket)
811
assert_raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (foo)" do
912
conn.bind(method: :foo)
1013
end

Diff for: test/test_ldap.rb

+31-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
require 'test_helper'
22

33
class TestLDAPInstrumentation < Test::Unit::TestCase
4+
# Fake Net::LDAP::Connection for testing
5+
class FakeConnection
6+
# It's difficult to instantiate Net::LDAP::PDU objects. Faking out what we
7+
# need here until that object is brought under test and has it's constructor
8+
# cleaned up.
9+
class Result < Struct.new(:success?, :result_code); end
10+
11+
def initialize
12+
@bind_success = Result.new(true, Net::LDAP::ResultCodeSuccess)
13+
@search_success = Result.new(true, Net::LDAP::ResultCodeSizeLimitExceeded)
14+
end
15+
16+
def bind(args = {})
17+
@bind_success
18+
end
19+
20+
def search(*args)
21+
yield @search_success if block_given?
22+
@search_success
23+
end
24+
end
25+
426
def setup
527
@connection = flexmock(:connection, :close => true)
628
flexmock(Net::LDAP::Connection).should_receive(:new).and_return(@connection)
@@ -15,8 +37,9 @@ def setup
1537
def test_instrument_bind
1638
events = @service.subscribe "bind.net_ldap"
1739

18-
bind_result = flexmock(:bind_result, :success? => true)
19-
flexmock(@connection).should_receive(:bind).with(Hash).and_return(bind_result)
40+
fake_connection = FakeConnection.new
41+
@subject.connection = fake_connection
42+
bind_result = fake_connection.bind
2043

2144
assert @subject.bind
2245

@@ -28,10 +51,9 @@ def test_instrument_bind
2851
def test_instrument_search
2952
events = @service.subscribe "search.net_ldap"
3053

31-
flexmock(@connection).should_receive(:bind).and_return(flexmock(:bind_result, :result_code => Net::LDAP::ResultCodeSuccess))
32-
flexmock(@connection).should_receive(:search).with(Hash, Proc).
33-
yields(entry = Net::LDAP::Entry.new("uid=user1,ou=users,dc=example,dc=com")).
34-
and_return(flexmock(:search_result, :success? => true, :result_code => Net::LDAP::ResultCodeSuccess))
54+
fake_connection = FakeConnection.new
55+
@subject.connection = fake_connection
56+
entry = fake_connection.search
3557

3658
refute_nil @subject.search(:filter => "(uid=user1)")
3759

@@ -44,10 +66,9 @@ def test_instrument_search
4466
def test_instrument_search_with_size
4567
events = @service.subscribe "search.net_ldap"
4668

47-
flexmock(@connection).should_receive(:bind).and_return(flexmock(:bind_result, :result_code => Net::LDAP::ResultCodeSuccess))
48-
flexmock(@connection).should_receive(:search).with(Hash, Proc).
49-
yields(entry = Net::LDAP::Entry.new("uid=user1,ou=users,dc=example,dc=com")).
50-
and_return(flexmock(:search_result, :success? => true, :result_code => Net::LDAP::ResultCodeSizeLimitExceeded))
69+
fake_connection = FakeConnection.new
70+
@subject.connection = fake_connection
71+
entry = fake_connection.search
5172

5273
refute_nil @subject.search(:filter => "(uid=user1)", :size => 1)
5374

Diff for: test/test_ldap_connection.rb

+44-33
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,55 @@ def capture_stderr
99
$stderr = stderr
1010
end
1111

12+
# Fake socket for testing
13+
#
14+
# FakeTCPSocket.new("success", 636)
15+
# FakeTCPSocket.new("fail.SocketError", 636) # raises SocketError
16+
class FakeTCPSocket
17+
def initialize(host, port, socket_opts = {})
18+
status, error = host.split(".")
19+
if status == "fail"
20+
raise Object.const_get(error)
21+
end
22+
end
23+
end
24+
1225
def test_list_of_hosts_with_first_host_successful
1326
hosts = [
14-
['test.mocked.com', 636],
15-
['test2.mocked.com', 636],
16-
['test3.mocked.com', 636],
27+
["success.host", 636],
28+
["fail.SocketError", 636],
29+
["fail.SocketError", 636],
1730
]
18-
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[0], { connect_timeout: 5 }).once.and_return(nil)
19-
flexmock(Socket).should_receive(:tcp).ordered.never
20-
Net::LDAP::Connection.new(:hosts => hosts)
31+
32+
connection = Net::LDAP::Connection.new(:hosts => hosts, :socket_class => FakeTCPSocket)
33+
connection.socket
2134
end
2235

2336
def test_list_of_hosts_with_first_host_failure
2437
hosts = [
25-
['test.mocked.com', 636],
26-
['test2.mocked.com', 636],
27-
['test3.mocked.com', 636],
38+
["fail.SocketError", 636],
39+
["success.host", 636],
40+
["fail.SocketError", 636],
2841
]
29-
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[0], { connect_timeout: 5 }).once.and_raise(SocketError)
30-
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[1], { connect_timeout: 5 }).once.and_return(nil)
31-
flexmock(Socket).should_receive(:tcp).ordered.never
32-
Net::LDAP::Connection.new(:hosts => hosts)
42+
43+
connection = Net::LDAP::Connection.new(:hosts => hosts, :socket_class => FakeTCPSocket)
44+
connection.socket
3345
end
3446

3547
def test_list_of_hosts_with_all_hosts_failure
3648
hosts = [
37-
['test.mocked.com', 636],
38-
['test2.mocked.com', 636],
39-
['test3.mocked.com', 636],
49+
["fail.SocketError", 636],
50+
["fail.SocketError", 636],
51+
["fail.SocketError", 636],
4052
]
41-
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[0], { connect_timeout: 5 }).once.and_raise(SocketError)
42-
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[1], { connect_timeout: 5 }).once.and_raise(SocketError)
43-
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[2], { connect_timeout: 5 }).once.and_raise(SocketError)
44-
flexmock(Socket).should_receive(:tcp).ordered.never
53+
54+
connection = Net::LDAP::Connection.new(:hosts => hosts, :socket_class => FakeTCPSocket)
4555
assert_raise Net::LDAP::ConnectionError do
46-
Net::LDAP::Connection.new(:hosts => hosts)
56+
connection.socket
4757
end
4858
end
4959

60+
# This belongs in test_ldap, not test_ldap_connection
5061
def test_result_for_connection_failed_is_set
5162
flexmock(Socket).should_receive(:tcp).and_raise(Errno::ECONNREFUSED)
5263

@@ -61,42 +72,42 @@ def test_result_for_connection_failed_is_set
6172
end
6273

6374
def test_unresponsive_host
75+
connection = Net::LDAP::Connection.new(:host => "fail.Errno::ETIMEDOUT", :port => 636, :socket_class => FakeTCPSocket)
6476
assert_raise Net::LDAP::Error do
65-
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
77+
connection.socket
6678
end
6779
end
6880

6981
def test_blocked_port
70-
flexmock(Socket).should_receive(:tcp).and_raise(SocketError)
82+
connection = Net::LDAP::Connection.new(:host => "fail.SocketError", :port => 636, :socket_class => FakeTCPSocket)
7183
assert_raise Net::LDAP::Error do
72-
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
84+
connection.socket
7385
end
7486
end
7587

7688
def test_connection_refused
77-
flexmock(Socket).should_receive(:tcp).and_raise(Errno::ECONNREFUSED)
89+
connection = Net::LDAP::Connection.new(:host => "fail.Errno::ECONNREFUSED", :port => 636, :socket_class => FakeTCPSocket)
7890
stderr = capture_stderr do
7991
assert_raise Net::LDAP::ConnectionRefusedError do
80-
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
92+
connection.socket
8193
end
8294
end
8395
assert_equal("Deprecation warning: Net::LDAP::ConnectionRefused will be deprecated. Use Errno::ECONNREFUSED instead.\n", stderr)
8496
end
8597

86-
def test_connection_timedout
87-
flexmock(Socket).should_receive(:tcp).and_raise(Errno::ETIMEDOUT)
98+
def test_connection_timeout
99+
connection = Net::LDAP::Connection.new(:host => "fail.Errno::ETIMEDOUT", :port => 636, :socket_class => FakeTCPSocket)
88100
stderr = capture_stderr do
89101
assert_raise Net::LDAP::Error do
90-
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
102+
connection.socket
91103
end
92104
end
93105
end
94106

95107
def test_raises_unknown_exceptions
96-
error = Class.new(StandardError)
97-
flexmock(Socket).should_receive(:tcp).and_raise(error)
98-
assert_raise error do
99-
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
108+
connection = Net::LDAP::Connection.new(:host => "fail.StandardError", :port => 636, :socket_class => FakeTCPSocket)
109+
assert_raise StandardError do
110+
connection.socket
100111
end
101112
end
102113

0 commit comments

Comments
 (0)