net-dnd 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,114 @@
1
+ folder_path = File.dirname(__FILE__)
2
+ require "#{folder_path}/connection"
3
+ require "#{folder_path}/profile"
4
+ require "#{folder_path}/field"
5
+ require "#{folder_path}/errors"
6
+
7
+ module Net
8
+ module DND
9
+
10
+ # This is the DND session object. It provides the high-level interface for performing
11
+ # lookup-style searches against a given DND host. It manages the Connection object,
12
+ # which is the low-level socket stuff, and sends back DND Profile objects, either singly or
13
+ # an array, as the result of the find method.
14
+
15
+ class Session
16
+
17
+ attr_reader :fields, :connection
18
+
19
+ # Constructor method. Only called directly by unit tests (specs). The start method
20
+ # handles the setting up of a full DND session.
21
+
22
+ def initialize(host)
23
+ @connection = Connection.new(host)
24
+ raise ConnectionError, connection.error unless connection.open?
25
+ end
26
+
27
+ # Starts a new DND session/connection. Called by the module-level start methods.
28
+
29
+ def self.start(host, field_list=[])
30
+ session = Session.new(host)
31
+ session.set_fields(field_list)
32
+ session
33
+ end
34
+
35
+ # Are we still open?
36
+
37
+ def open?
38
+ connection.open?
39
+ end
40
+
41
+ # Set the list of fields used by subsequent find commands. These fields are then passed
42
+ # to the Profile.new method when one or more users are returned by the find command. This
43
+ # method will raise a couple of error messages, that you might want to trap for:
44
+ #
45
+ # FieldNotFound is raised when a specified field is not found in the list of fields known
46
+ # by the currently connected to DND server.
47
+ #
48
+ # FieldAccessDenied is raised when a speficied field is one whose value is not world
49
+ # readable, meaning you need to be in an authenticated session to access it.
50
+ #
51
+ # You can manually send the set_fields command, if you happen to need to change the list of
52
+ # fields returned by your find commands, after you've instantiated the session object.
53
+
54
+ def set_fields(field_list=[])
55
+ response = request(:fields, field_list)
56
+ @fields = []
57
+ raise FieldNotFound, response.error unless response.ok?
58
+ response.items.each do |item|
59
+ field = Field.from_field_line(item)
60
+ if field.read_all? # only world readable fields are valid for DND Profiles
61
+ @fields << field.to_sym
62
+ else
63
+ raise FieldAccessDenied, "#{field.to_s} is not world readable." unless field_list.empty?
64
+ end
65
+ end
66
+ end
67
+
68
+ # The find command is the real reason for the Net::DND libray. It provides the ability to
69
+ # send a 'user specifier' to a connected DND server and then parse the returned data into
70
+ # one or more Net::DND::Profile objects.
71
+ #
72
+ # You can send the find command in two flavors: the first, when you simply submit the look_for
73
+ # argument will assume that you're expecting more than one user to match the look_for string.
74
+ # Thus it will always return a array as its result. This array will contain zero, one or more
75
+ # Profile objects.
76
+ #
77
+ # In it's second flavor, you are submitting a value for the 'one' argument. Normally, this
78
+ # means you've sent a :one as the second argument to the call, but any non-false value will
79
+ # work. When called in this manner, you're telling the Session that you only want a Profile
80
+ # object if your 'look_for' returns a single match, otherwise the find will return nil. This
81
+ # flavor is recommended when you are performing a find using a 'uid' or a 'dctsnum' value.
82
+
83
+ def find(look_for, one=nil)
84
+ response = request(:lookup, look_for.to_s, fields)
85
+ if one
86
+ return nil unless response.items.length == 1
87
+ Profile.new(fields, response.items[0])
88
+ else
89
+ response.items.map { |item| Profile.new(fields, item) }
90
+ end
91
+ end
92
+
93
+ # The manual session close command. You only call this method if you aren't using the block
94
+ # version of the module 'start' command. If you use the block, it will automatically close
95
+ # the session when the block exits.
96
+
97
+ def close
98
+ request(:quit, nil)
99
+ end
100
+
101
+ private
102
+
103
+ # This method handles the sending of the raw protocol commands to the Connection object. It
104
+ # will only send commands if the Session is still 'open'. It always returns the Response object
105
+ # back from the result of calling into the Connection.
106
+
107
+ def request(type, *args)
108
+ raise ConnectionClosed, "Connection closed." unless open?
109
+ response = connection.send(type, *args)
110
+ end
111
+
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,56 @@
1
+ module Net
2
+ module DND
3
+
4
+ # This is a container class for the User Specifier portion of the DND protocol lookup command.
5
+ # Something like this isn't expressly needed, but because there are 3 types of specifier,
6
+ # based around 4 different patterns of specifier, which leads to three slightly different
7
+ # types of output, it seemed like a class was the best way to abstract those determinations.
8
+
9
+ # Once a string specifier is passed into the constructer method, it's stored and then
10
+ # matched against one of several patterns to determine it's type. This type is then used
11
+ # to choose the output format of the specifier, when the instantiated class object is
12
+ # coerced back to a string.
13
+
14
+ class UserSpec
15
+
16
+ attr_reader :type
17
+
18
+ # Construct our specifier object and set its type attribute.
19
+
20
+ def initialize(specifier)
21
+ @spec = specifier.to_s
22
+ @type = case @spec.downcase
23
+ when /^\d+$/
24
+ :uid
25
+ when /^z\d+$/
26
+ :did
27
+ when /^\d+[a-z]\d*$/
28
+ :did
29
+ else
30
+ :name
31
+ end
32
+ end
33
+
34
+ # Output the correct 'string' format for our specifier. The :uid and :did types have
35
+ # special characters prepended to their value, so that they are correctly formatted
36
+ # for use in a DND connection/protocol lookup command.
37
+
38
+ def to_s
39
+ case @type
40
+ when :uid
41
+ "##{@spec}"
42
+ when :did
43
+ "#*#{@spec}"
44
+ else
45
+ @spec
46
+ end
47
+ end
48
+
49
+ # Inspection string for the specifier object.
50
+
51
+ def inspect
52
+ "#<#{self.class} specifier=\"#{@spec}\" type=:#{@type}>"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,11 @@
1
+ module Net
2
+ module DND
3
+
4
+ MAJOR = 1
5
+ MINOR = 1
6
+ PATCH = 2
7
+
8
+ VERSION = [MAJOR, MINOR, PATCH].join(".")
9
+
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "net/dnd/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "net-dnd"
7
+ s.version = Net::DND::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Brian V. Hughes"]
10
+ s.email = ["brianvh@dartmouth.edu"]
11
+ s.homepage = %(https://github.com/brianvh/net-dnd/)
12
+ s.summary = %(#{s.name}-#{s.version})
13
+ s.description = %(Ruby library for DND lookups.)
14
+
15
+ s.required_rubygems_version = ">= 1.3.7"
16
+ s.rubyforge_project = "net-dnd"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ s.add_development_dependency 'bundler', '~> 1.0.10'
24
+ s.add_development_dependency 'rspec', '~> 2.5.0'
25
+ end
@@ -0,0 +1,94 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'net/dnd/connection'
3
+
4
+ module Net ; module DND
5
+
6
+ describe "a good socket", :shared => true do
7
+ before(:each) do
8
+ @socket = flexmock("TCP Socket")
9
+ @tcp = flexmock(TCPSocket)
10
+ @response = flexmock(Response)
11
+ end
12
+ end
13
+
14
+ describe Connection, "to a bad host" do
15
+
16
+ it_should_behave_like "a good socket"
17
+
18
+ before(:each) do
19
+ @tcp.should_receive(:open).and_raise(Errno::ECONNREFUSED, "Connection refused")
20
+ @connection = Connection.new('my.fakehost.com')
21
+ end
22
+
23
+ it "should not indicate an open connection" do
24
+ @connection.should_not be_open
25
+ end
26
+
27
+ it "should return the 'Could not connect' error message" do
28
+ @connection.error.should match(/^Could not connect to/)
29
+ end
30
+ end
31
+
32
+ describe Connection, "to a busy/slow host" do
33
+
34
+ it_should_behave_like "a good socket"
35
+
36
+ before(:each) do
37
+ flexmock(Timeout).should_receive(:timeout).and_raise(Timeout::Error, "Connection timed out")
38
+ @connection = Connection.new('my.slowhost.com')
39
+ end
40
+
41
+ it "should not indicate an open connection" do
42
+ @connection.should_not be_open
43
+ end
44
+
45
+ it "should return the 'Connection timed out' error message" do
46
+ @connection.error.should match(/^Connection attempt .* has timed out/)
47
+ end
48
+ end
49
+
50
+ describe Connection, "to a good host" do
51
+
52
+ it_should_behave_like "a good socket"
53
+
54
+ before(:each) do
55
+ @tcp.should_receive(:open).once.and_return(@socket)
56
+ @response.should_receive(:process).at_least.once.and_return(@response)
57
+ @response.should_receive(:ok?).at_least.once.and_return(true)
58
+ @connection = Connection.new('my.goodhost.com')
59
+ end
60
+
61
+ it "should indicate an open connection" do
62
+ @connection.should be_open
63
+ end
64
+
65
+ it "should not have any error messages" do
66
+ @connection.error.should be_nil
67
+ end
68
+
69
+ describe "sending commands" do
70
+
71
+ it "should send the correct command when fields is called with empty field list" do
72
+ @socket.should_receive(:puts).once.with('fields')
73
+ @connection.fields
74
+ end
75
+
76
+ it "should send the correct command when fields is called with a field list" do
77
+ @socket.should_receive(:puts).once.with('fields name nickname')
78
+ @connection.fields(['name', 'nickname'])
79
+ end
80
+
81
+ it "should send the correct command when lookup is called" do
82
+ @socket.should_receive(:puts).once.with('lookup joe user,name nickname')
83
+ @connection.lookup('joe user', ['name', 'nickname'])
84
+ end
85
+
86
+ it "should send the correct command when quit is called" do
87
+ @socket.should_receive(:puts).once.with('quit')
88
+ @socket.should_receive(:close)
89
+ @connection.quit
90
+ end
91
+ end
92
+ end
93
+
94
+ end ; end
@@ -0,0 +1,72 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'net/dnd/field'
3
+
4
+ module Net ; module DND
5
+
6
+ describe Field, "created normally" do
7
+ before :each do
8
+ @name, @write, @read = %w(nickname U A)
9
+ @field = Field.new(@name, @write, @read)
10
+ end
11
+
12
+ it "should properly set the writeable flag" do
13
+ @field.writeable.should == @write
14
+ end
15
+
16
+ it "should properly set the readable flag" do
17
+ @field.readable.should == @read
18
+ end
19
+
20
+ it "should report as readable by all if readable value is 'A'" do
21
+ @field.should be_read_all
22
+ end
23
+
24
+ it "should report back the proper name" do
25
+ @field.name.should == @name
26
+ end
27
+
28
+ it "should report back the proper inspection string" do
29
+ @field.inspect.should match(/<Net::DND::Field name=".*" writeable="[AUNT]" readable="[AUNT]">/)
30
+ end
31
+
32
+ it "should return the name when coerced to a string" do
33
+ @field.to_s.should == @name
34
+ end
35
+
36
+ it "should return :name when coerced to a symbol" do
37
+ @field.to_sym.should == @name.to_sym
38
+ end
39
+
40
+ it "should not report as readable by all if readable value is not 'A'" do
41
+ @read = "T"
42
+ @field = Field.new(@name, @write, @read)
43
+ @field.should_not be_read_all
44
+ end
45
+
46
+ end
47
+
48
+ describe Field, "created using from_field_line with a proper line format" do
49
+
50
+ before(:each) do
51
+ @values = %w(nickname U A)
52
+ line = @values.join(" ")
53
+ @field = Field.from_field_line(line)
54
+ end
55
+
56
+ it "should have the correct name" do
57
+ @field.name.should == @values[0]
58
+ end
59
+
60
+ it "should have to correct readable value" do
61
+ @field.readable.should == @values[2]
62
+ end
63
+ end
64
+
65
+ describe Field, "created using from_field_line with an improper line format" do
66
+ it "should raise the proper error" do
67
+ line = "This is a bad field line"
68
+ lambda { Field.from_field_line(line) }.should raise_error(FieldLineInvalid)
69
+ end
70
+ end
71
+
72
+ end ; end
@@ -0,0 +1,66 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'net/dnd/profile'
3
+
4
+ module Net ; module DND
5
+
6
+ describe Profile, "for Joe D. User" do
7
+
8
+ before(:each) do
9
+ @fields = [:name, :nickname, :deptclass, :email]
10
+ @items = ['Joe D. User', 'joey jdu', 'Student', 'Joe.D.User@Dartmouth.edu']
11
+ @profile = Profile.new(@fields, @items)
12
+ end
13
+
14
+ it "should return the correct object" do
15
+ @profile.should be_instance_of(Profile)
16
+ end
17
+
18
+ it "should return the correct inspection string" do
19
+ @profile.inspect.should match(/<Net::DND::Profile length=4, .*deptclass="Student".*>/)
20
+ end
21
+
22
+ it "should have the correct number of entries" do
23
+ @profile.length.should == 4
24
+ end
25
+
26
+ it "should return the correct name" do
27
+ @profile.name.should == @items[0]
28
+ end
29
+
30
+ it "should return the correct email" do
31
+ @profile[:email].should == @items[3]
32
+ end
33
+
34
+ it "should contain nickname field" do
35
+ @profile.should be_nickname
36
+ end
37
+
38
+ it "should not contain did field" do
39
+ @profile.should_not be_did
40
+ end
41
+
42
+ it "should raise Field Not Found error if did field is requested" do
43
+ lambda { @profile.did }.should raise_error(FieldNotFound)
44
+ end
45
+
46
+ end
47
+
48
+ describe Profile, "for Joe D. Expired" do
49
+
50
+ before(:each) do
51
+ @fields = [:name, :expires]
52
+ @items = ['Joe D. User', '01-Jan-2010']
53
+ @profile = Profile.new(@fields, @items)
54
+ end
55
+
56
+ it "should have a valid expire_date" do
57
+ @profile.expires_on.should_not be_nil
58
+ end
59
+
60
+ it "should be expired" do
61
+ @profile.should be_expired
62
+ end
63
+
64
+ end
65
+
66
+ end ; end
@@ -0,0 +1,161 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'net/dnd/response'
3
+
4
+ module Net ; module DND
5
+
6
+ describe Response, "on initial create" do
7
+ before(:each) do
8
+ @socket = flexmock("TCP Socket")
9
+ @response = Response.new(@socket)
10
+ end
11
+
12
+ it "should have no :code value" do
13
+ @response.code.should be_nil
14
+ end
15
+
16
+ it "should have no :error value" do
17
+ @response.error.should be_nil
18
+ end
19
+
20
+ it "should have an empty :items value" do
21
+ @response.items.should be_empty
22
+ end
23
+ end
24
+
25
+ describe Response, "after create status to a good socket" do
26
+ before(:each) do
27
+ @socket = flexmock("TCP Socket")
28
+ @response = Response.new(@socket)
29
+ @socket.should_receive(:gets).once.and_return('220 DND server ready.')
30
+ @response.status_line
31
+ end
32
+
33
+ it "should have a :code of 220" do
34
+ @response.code.should == 220
35
+ end
36
+
37
+ it do
38
+ @response.should be_ok
39
+ end
40
+ end
41
+
42
+ describe Response, "parsing a bad command" do
43
+
44
+ before(:each) do
45
+ @code = 501
46
+ @msg = "unknown field name foo"
47
+ @socket = flexmock("DND Socket after bad :fields command")
48
+ @socket.should_receive(:gets).once.and_return("#{@code} #{@msg}\r\n")
49
+ @response = Response.process(@socket)
50
+ end
51
+
52
+ it "should return a code of 501" do
53
+ @response.code == @code
54
+ end
55
+
56
+ it "should have the appropriate error message" do
57
+ @response.error == @msg
58
+ end
59
+
60
+ it do
61
+ @response.should_not be_ok
62
+ end
63
+ end
64
+
65
+ describe Response, "parsing a :quit command" do
66
+
67
+ before(:each) do
68
+ @code = 200
69
+ @msg = "Ok"
70
+ @socket = flexmock("DND Socket after :quit command")
71
+ @socket.should_receive(:gets).once.and_return("#{@code} #{@msg}\r\n")
72
+ @response = Response.process(@socket)
73
+ end
74
+
75
+ it "should return a code of 200" do
76
+ @response.code == @code
77
+ end
78
+
79
+ it do
80
+ @response.should be_ok
81
+ end
82
+ end
83
+
84
+ describe Response, "parsing a :fields command" do
85
+
86
+ before(:each) do
87
+ @code = [102, 200]
88
+ @count = 2
89
+ @data = ['120 name N A', '120 nickname U A']
90
+ @status = 'Done'
91
+ @socket = flexmock("DND Socket after :fields command")
92
+ @socket.should_receive(:gets).times(4).and_return(
93
+ "#{@code[0]} #{@count}\r\n", "#{@data[0]}\r\n",
94
+ "#{@data[1]}\r\n", "#{@code[1]} #{@status}\r\n")
95
+ @response = Response.process(@socket)
96
+ end
97
+
98
+ it "should have a sub_count of 0" do
99
+ @response.sub_count == 0
100
+ end
101
+
102
+ it "should have the correct number of items" do
103
+ @response.should have(2).items
104
+ end
105
+
106
+ it "should have 'nickname' as the second item" do
107
+ @response.items[1].split[0] == 'nickname'
108
+ end
109
+
110
+ it "should have a code of 200" do
111
+ @response.code.should == @code[1]
112
+ end
113
+
114
+ it do
115
+ @response.should be_ok
116
+ end
117
+ end
118
+
119
+ describe Response, "parsing a :lookup command" do
120
+
121
+ before(:each) do
122
+ @code = [102, 201]
123
+ @count = 2
124
+ @sub_count = 2
125
+ @data = ['110 Joe Q. User', '110 joey, jqu', '110 Jane P. User', '110 janes, jp']
126
+ @status = 'Additional matches not returned'
127
+ @socket = flexmock("DND Socket after :lookup command")
128
+ @socket.should_receive(:gets).times(6).and_return(
129
+ "#{@code[0]} #{@count} #{@sub_count}\r\n",
130
+ "#{@data[0]}\r\n", "#{@data[1]}\r\n",
131
+ "#{@data[2]}\r\n", "#{@data[3]}\r\n",
132
+ "#{@code[1]} #{@status}\r\n")
133
+ @response = Response.process(@socket)
134
+ end
135
+
136
+ it "should have the correct count" do
137
+ @response.count == @count
138
+ end
139
+
140
+ it "should have the correct sub_count" do
141
+ @response.sub_count == @sub_count
142
+ end
143
+
144
+ it "should have items stored as an array of arrays" do
145
+ @response.items[0].should be_an_instance_of(Array)
146
+ end
147
+
148
+ it "should have the correct name for the sub-array of the second item" do
149
+ @response.items[1][0] == 'Jane P. User'
150
+ end
151
+
152
+ it "should have a code of 201" do
153
+ @response.code.should == @code[1]
154
+ end
155
+
156
+ it do
157
+ @response.should be_ok
158
+ end
159
+ end
160
+
161
+ end; end