net-dnd 1.1.2

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.
@@ -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