roust 1.4.1 → 1.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d1a2f356ca5640d7d394f07ced73cc03abcd6e34
4
- data.tar.gz: 41196d852637d8c592068c6e1faf993183349a5a
3
+ metadata.gz: ecda9bb4e79252eb39c2839ff0b8ae25f4069817
4
+ data.tar.gz: fc627b8cb18996b1658249f0cf5d434a219f1a90
5
5
  SHA512:
6
- metadata.gz: 197a802ad243cafcff1965016d544b1722dd83ef9d633410ede414422a0ffba1de3e63f4d91f548ac546501f4c898bda05f459f50c255495c3a11252ee326089
7
- data.tar.gz: 5a68904147845e22f3ff138eb0bf1d0a6d5f64da303d7cc877706ac57a132d7bea1ef7ef5122ea4bbe128a5ed44b8f77adc990a6283334a9461b04cda8ad2725
6
+ metadata.gz: 5773318cc0aad6e9f4da047481c7f3aecd760854f9d94dc6cefe3a2751f0c984ca0ca141dce9eb80da50468d2dfcb1b9a1b9f3ed064353d4a8e075d94edb22fd
7
+ data.tar.gz: eaa2ffe6bbcb5ff81ffa52c759d65a3af9cc34b85c781538a536ead7494806c61eed87471edf0b875f2d6f41b686daf80dfd7ccbea76f73599d1987faa6ecc73
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- roust (1.4.1)
4
+ roust (1.5.0)
5
5
  activesupport (>= 4.1.0)
6
6
  httparty (>= 0.13.1)
7
7
  mail (>= 2.5.4)
data/lib/roust/queue.rb CHANGED
@@ -1,21 +1,17 @@
1
- module Roust::Queue
2
- # id can be numeric (e.g. 28) or textual (e.g. sales)
3
- def queue_show(id)
4
- response = self.class.get("/queue/#{id}")
1
+ class Roust
2
+ module Queue
3
+ # id can be numeric (e.g. 28) or textual (e.g. sales)
4
+ def queue_show(id)
5
+ response = self.class.get("/queue/#{id}")
5
6
 
6
- body, _ = explode_response(response)
7
- if body =~ /No queue named/
8
- nil
9
- else
10
- body.gsub!(/\n\s*\n/, "\n") # remove blank lines for Mail
11
- message = Mail.new(body)
12
- Hash[message.header.fields.map { |header|
13
- key = header.name.to_s.downcase
14
- value = header.value.to_s
15
- [ key, value ]
16
- }]
7
+ body, _ = explode_response(response)
8
+ if body =~ /No queue named/
9
+ nil
10
+ else
11
+ body_to_hash(body)
12
+ end
17
13
  end
18
- end
19
14
 
20
- alias_method :queue, :queue_show
15
+ alias_method :queue, :queue_show
16
+ end
21
17
  end
data/lib/roust/ticket.rb CHANGED
@@ -1,245 +1,241 @@
1
- module Roust::Ticket
2
- def ticket_show(id)
3
- response = self.class.get("/ticket/#{id}/show")
1
+ class Roust
2
+ module Ticket
3
+ def ticket_show(id)
4
+ response = self.class.get("/ticket/#{id}/show")
4
5
 
5
- body, _ = explode_response(response)
6
+ body, _ = explode_response(response)
6
7
 
7
- return nil if body =~ /^# (Ticket (\d+) does not exist\.)/
8
+ return nil if body =~ /^# (Ticket (\d+) does not exist\.)/
8
9
 
9
- # Replace CF spaces with underscores
10
- while body.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/)
11
- body.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
12
- end
10
+ # Replace CF spaces with underscores
11
+ while body.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/)
12
+ body.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
13
+ end
13
14
 
14
- # Sometimes the API returns requestors formatted like this:
15
- #
16
- # Requestors: foo@example.org,
17
- # bar@example.org, baz@example.org
18
- # qux@example.org, quux@example.org,
19
- # corge@example.org
20
- #
21
- # Turn it into this:
22
- #
23
- # Requestors: foo@example.org, bar@example.org, baz@example.org, ...
24
- #
25
- body.gsub!(/\n\n/, "\n")
26
-
27
- %w(Requestors Cc AdminCc).each do |field|
28
- body.gsub!(/^#{field}:(.+)^\n/m) do |m|
29
- m.strip.split(/,\s+/).join(', ').strip
15
+ # Sometimes the API returns requestors formatted like this:
16
+ #
17
+ # Requestors: foo@example.org,
18
+ # bar@example.org, baz@example.org
19
+ # qux@example.org, quux@example.org,
20
+ # corge@example.org
21
+ #
22
+ # Turn it into this:
23
+ #
24
+ # Requestors: foo@example.org, bar@example.org, baz@example.org, ...
25
+ #
26
+ body.gsub!(/\n\n/, "\n")
27
+
28
+ %w(Requestors Cc AdminCc).each do |field|
29
+ body.gsub!(/^#{field}:(.+)^\n/m) do |m|
30
+ m.strip.split(/,\s+/).join(', ').strip
31
+ end
30
32
  end
31
- end
32
-
33
- message = Mail.new(body)
34
33
 
35
- hash = Hash[message.header.fields.map { |header|
36
- key = header.name.to_s
37
- value = header.value.to_s
38
- [ key, value ]
39
- }]
34
+ hash = body_to_hash(body)
40
35
 
41
- %w(Requestors Cc AdminCc).each do |field|
42
- hash[field] = hash[field].split(', ') if hash[field]
43
- end
44
-
45
- hash['id'] = hash['id'].split('/').last
46
-
47
- hash
48
- end
49
-
50
- def ticket_create(attrs)
51
- default_attrs = {
52
- 'id' => 'ticket/new'
53
- }
54
- attrs = default_attrs.merge(attrs).stringify_keys!
55
-
56
- error = create_invalid?(attrs)
57
- raise InvalidRecord, error if error
58
-
59
- attrs['Text'].gsub!(/\n/, "\n ") if attrs['Text'] # insert a space on continuation lines.
36
+ %w(Requestors Cc AdminCc).each do |field|
37
+ hash[field] = hash[field].split(', ') if hash[field]
38
+ end
60
39
 
61
- # We can't set more than one AdminCc when creating a ticket. WTF RT.
62
- #
63
- # Delete it from the ticket we are creating, and we'll update the ticket
64
- # after we've created.
65
- admincc = attrs.delete('AdminCc')
40
+ hash['id'] = hash['id'].split('/').last
66
41
 
67
- content = compose_content('ticket', attrs['id'], attrs)
42
+ hash
43
+ end
68
44
 
69
- response = self.class.post(
70
- '/ticket/new',
71
- :body => {
72
- :content => content
45
+ def ticket_create(attrs)
46
+ default_attrs = {
47
+ 'id' => 'ticket/new'
73
48
  }
74
- )
75
-
76
- body, _ = explode_response(response)
77
-
78
- case body
79
- when /^# Ticket (\d+) created/
80
- id = body[/^# Ticket (\d+) created/, 1]
81
- # Add the AdminCc after the ticket is created, because we can't set it
82
- # on ticket creation.
83
- update(id, 'AdminCc' => admincc) if admincc
84
-
85
- # Return the whole ticket, not just the id.
86
- show(id)
87
- when /^# Could not create ticket/
88
- raise BadRequest, body
89
- when /^# Syntax error/
90
- raise SyntaxError, body
91
- else
92
- raise UnhandledResponse, body
49
+ attrs = default_attrs.merge(attrs).stringify_keys!
50
+
51
+ error = create_invalid?(attrs)
52
+ raise InvalidRecord, error if error
53
+
54
+ attrs['Text'].gsub!(/\n/, "\n ") if attrs['Text'] # insert a space on continuation lines.
55
+
56
+ # We can't set more than one AdminCc when creating a ticket. WTF RT.
57
+ #
58
+ # Delete it from the ticket we are creating, and we'll update the ticket
59
+ # after we've created.
60
+ admincc = attrs.delete('AdminCc')
61
+
62
+ content = compose_content('ticket', attrs['id'], attrs)
63
+
64
+ response = self.class.post(
65
+ '/ticket/new',
66
+ :body => {
67
+ :content => content
68
+ }
69
+ )
70
+
71
+ body, _ = explode_response(response)
72
+
73
+ case body
74
+ when /^# Ticket (\d+) created/
75
+ id = $1
76
+ # Add the AdminCc after the ticket is created, because we can't set it
77
+ # on ticket creation.
78
+ update(id, 'AdminCc' => admincc) if admincc
79
+
80
+ # Return the whole ticket, not just the id.
81
+ show(id)
82
+ when /^# Could not create ticket/
83
+ raise BadRequest, body
84
+ when /^# Syntax error/
85
+ raise SyntaxError, body
86
+ else
87
+ raise UnhandledResponse, body
88
+ end
93
89
  end
94
- end
95
90
 
96
- def ticket_update(id, attrs)
97
- content = compose_content('ticket', id, attrs)
98
-
99
- response = self.class.post(
100
- "/ticket/#{id}/edit",
101
- :body => {
102
- :content => content
103
- },
104
- )
105
-
106
- body, _ = explode_response(response)
107
-
108
- case body
109
- when /^# Ticket (\d+) updated/
110
- id = body[/^# Ticket (\d+) updated/, 1]
111
- show(id)
112
- when /^# You are not allowed to modify ticket \d+/
113
- raise Unauthorized, body
114
- when /^# Syntax error/
115
- raise SyntaxError, body
116
- else
117
- raise UnhandledResponse, body
91
+ def ticket_update(id, attrs)
92
+ content = compose_content('ticket', id, attrs)
93
+
94
+ response = self.class.post(
95
+ "/ticket/#{id}/edit",
96
+ :body => {
97
+ :content => content
98
+ },
99
+ )
100
+
101
+ body, _ = explode_response(response)
102
+
103
+ case body
104
+ when /^# Ticket (\d+) updated/
105
+ id = $1
106
+ show(id)
107
+ when /^# You are not allowed to modify ticket \d+/
108
+ raise Unauthorized, body
109
+ when /^# Syntax error/
110
+ raise SyntaxError, body
111
+ else
112
+ raise UnhandledResponse, body
113
+ end
118
114
  end
119
- end
120
115
 
121
- def ticket_search(query)
122
- params = {
123
- :query => query,
124
- :format => 's',
125
- :orderby => '+id'
126
- }
127
- response = self.class.get('/search/ticket', :query => params)
128
- # FIXME(auxesis) use explode_response here
129
-
130
- body, _ = explode_response(response)
131
- body.split("\n").map do |t|
132
- id, subject = t.split(': ', 2)
133
- {'id' => id, 'Subject' => subject}
116
+ def ticket_search(query)
117
+ params = {
118
+ :query => query,
119
+ :format => 's',
120
+ :orderby => '+id'
121
+ }
122
+ response = self.class.get('/search/ticket', :query => params)
123
+ # FIXME(auxesis) use explode_response here
124
+
125
+ body, _ = explode_response(response)
126
+ body.split("\n").map do |t|
127
+ id, subject = t.split(': ', 2)
128
+ {'id' => id, 'Subject' => subject}
129
+ end
134
130
  end
135
- end
136
131
 
137
- def ticket_history(id, opts = {})
138
- options = {
139
- :format => 'short',
140
- :comments => false
141
- }.merge(opts)
132
+ def ticket_history(id, opts = {})
133
+ options = {
134
+ :format => 'short',
135
+ :comments => false
136
+ }.merge(opts)
142
137
 
143
- format = options[:format]
144
- comments = options[:comments]
145
- params = {
146
- :format => format[0]
147
- }
138
+ format = options[:format]
139
+ comments = options[:comments]
140
+ params = {
141
+ :format => format[0]
142
+ }
148
143
 
149
- response = self.class.get("/ticket/#{id}/history", :query => params)
144
+ response = self.class.get("/ticket/#{id}/history", :query => params)
150
145
 
151
- body, _ = explode_response(response)
146
+ body, _ = explode_response(response)
152
147
 
153
- case format
154
- when 'short'
155
- parse_short_history(body, :comments => comments)
156
- when 'long'
157
- parse_long_history(body, :comments => comments)
148
+ case format
149
+ when 'short'
150
+ parse_short_history(body, :comments => comments)
151
+ when 'long'
152
+ parse_long_history(body, :comments => comments)
153
+ end
158
154
  end
159
- end
160
155
 
161
- # TODO(auxesis): add method for getting ticket links
162
- # TODO(auxesis): add method for updating ticket links
163
- # TODO(auxesis): add method for listing ticket attachments
164
- # TODO(auxesis): add method for getting a ticket attachment
165
- # TODO(auxesis): add method for commenting on a ticket
166
- # TODO(auxesis): add method for replying on a ticket
167
-
168
- # To maintain backwards compatibility with previous versions (and rt-client),
169
- # alias these methods to their short form.
170
- alias_method :create, :ticket_create
171
- alias_method :show, :ticket_show
172
- alias_method :update, :ticket_update
173
- alias_method :history, :ticket_history
174
- alias_method :search, :ticket_search
175
-
176
- private
177
-
178
- def create_invalid?(attrs)
179
- missing = %w(id Subject Queue).select { |k| !attrs.include?(k) }
180
-
181
- if missing.empty?
182
- return false
183
- else
184
- "Needs attributes: #{missing.join(', ')}"
156
+ # TODO(auxesis): add method for getting ticket links
157
+ # TODO(auxesis): add method for updating ticket links
158
+ # TODO(auxesis): add method for listing ticket attachments
159
+ # TODO(auxesis): add method for getting a ticket attachment
160
+ # TODO(auxesis): add method for commenting on a ticket
161
+ # TODO(auxesis): add method for replying on a ticket
162
+
163
+ # To maintain backwards compatibility with previous versions (and rt-client),
164
+ # alias these methods to their short form.
165
+ alias_method :create, :ticket_create
166
+ alias_method :show, :ticket_show
167
+ alias_method :update, :ticket_update
168
+ alias_method :history, :ticket_history
169
+ alias_method :search, :ticket_search
170
+
171
+ private
172
+
173
+ def create_invalid?(attrs)
174
+ missing = %w(id Subject Queue).select { |k| !attrs.include?(k) }
175
+
176
+ if missing.empty?
177
+ return false
178
+ else
179
+ "Needs attributes: #{missing.join(', ')}"
180
+ end
185
181
  end
186
- end
187
182
 
188
- def parse_short_history(body, opts = {})
189
- comments = opts[:comments]
190
- regex = comments ? '^\d+:' : '^\d+: [^Comments]'
191
- history = body.split("\n").select { |l| l =~ /#{regex}/ }
192
- history.map { |l| l.split(': ', 2) }
193
- end
183
+ def parse_short_history(body, opts = {})
184
+ comments = opts[:comments]
185
+ regex = comments ? '^\d+:' : '^\d+: [^Comments]'
186
+ history = body.split("\n").select { |l| l =~ /#{regex}/ }
187
+ history.map { |l| l.split(': ', 2) }
188
+ end
194
189
 
195
- def parse_long_history(body, opts = {})
196
- comments = opts[:comments]
197
- items = body.split("\n--\n")
198
- list = []
199
- items.each do |item|
200
- # Yes, this messes with the "content:" field but that's the one that's upsetting Mail.new
201
- item.gsub!(/\n\s*\n/, "\n") # remove blank lines for Mail
202
- history = Mail.new(item)
203
- next if not comments and history['type'].to_s =~ /Comment/ # skip comments
204
- reply = {}
205
-
206
- history.header.fields.each_with_index do |header, index|
207
- next if index == 0
208
-
209
- key = header.name.to_s.downcase
210
- value = header.value.to_s
211
-
212
- attachments = []
213
- case key
214
- when 'attachments'
215
- temp = item.match(/Attachments:\s*(.*)/m)
216
- if temp.class != NilClass
217
- atarr = temp[1].split("\n")
218
- atarr.map { |a| a.gsub!(/^\s*/, '') }
219
- atarr.each do |a|
220
- i = a.match(/(\d+):\s*(.*)/)
221
- s = {
222
- :id => i[1].to_s,
223
- :name => i[2].to_s
224
- }
225
- sz = i[2].match(/(.*?)\s*\((.*?)\)/)
226
- if sz.class == MatchData
227
- s[:name] = sz[1].to_s
228
- s[:size] = sz[2].to_s
190
+ def parse_long_history(body, opts = {})
191
+ comments = opts[:comments]
192
+ items = body.split("\n--\n")
193
+ list = []
194
+ items.each do |item|
195
+ # Yes, this messes with the "content:" field but that's the one that's upsetting Mail.new
196
+ item.gsub!(/\n\s*\n/, "\n") # remove blank lines for Mail
197
+ history = Mail.new(item)
198
+ next if not comments and history['type'].to_s =~ /Comment/ # skip comments
199
+ reply = {}
200
+
201
+ history.header.fields.each_with_index do |header, index|
202
+ next if index == 0
203
+
204
+ key = header.name.to_s.downcase
205
+ value = header.value.to_s
206
+
207
+ attachments = []
208
+ case key
209
+ when 'attachments'
210
+ temp = item.match(/Attachments:\s*(.*)/m)
211
+ if temp.class != NilClass
212
+ atarr = temp[1].split("\n")
213
+ atarr.map { |a| a.gsub!(/^\s*/, '') }
214
+ atarr.each do |a|
215
+ i = a.match(/(\d+):\s*(.*)/)
216
+ s = {
217
+ :id => i[1].to_s,
218
+ :name => i[2].to_s
219
+ }
220
+ sz = i[2].match(/(.*?)\s*\((.*?)\)/)
221
+ if sz.class == MatchData
222
+ s[:name] = sz[1].to_s
223
+ s[:size] = sz[2].to_s
224
+ end
225
+ attachments << s
229
226
  end
230
- attachments << s
227
+ reply['attachments'] = attachments
231
228
  end
232
- reply['attachments'] = attachments
229
+ when 'content'
230
+ reply['content'] = value
231
+ else
232
+ reply["#{key}"] = value
233
233
  end
234
- when 'content'
235
- reply['content'] = value
236
- else
237
- reply["#{key}"] = value
238
234
  end
235
+ list << reply
239
236
  end
240
- list << reply
241
- end
242
237
 
243
- list
238
+ list
239
+ end
244
240
  end
245
241
  end
data/lib/roust/user.rb CHANGED
@@ -1,48 +1,109 @@
1
- module Roust::User
2
- # id can be numeric (e.g. 28) or textual (e.g. john)
3
- def user_show(id)
4
- response = self.class.get("/user/#{id}")
5
-
6
- body, _ = explode_response(response)
7
- if body =~ /No user named/
8
- nil
9
- else
10
- body.gsub!(/\n\s*\n/, "\n") # remove blank lines for Mail
11
- message = Mail.new(body)
12
- Hash[message.header.fields.map { |header|
13
- key = header.name.to_s.downcase
14
- value = header.value.to_s
15
- [ key, value ]
16
- }]
1
+ class Roust
2
+ module User
3
+ # id can be numeric (e.g. 28) or textual (e.g. john)
4
+ def user_show(id)
5
+ response = self.class.get("/user/#{id}")
6
+
7
+ body, _ = explode_response(response)
8
+ if body =~ /No user named/
9
+ nil
10
+ else
11
+ hash = body_to_hash(body)
12
+ convert_response_boolean_attrs(hash)
13
+ end
17
14
  end
18
- end
19
15
 
20
- def user_update(id, attrs)
21
- content = compose_content('user', id, attrs)
16
+ def user_update(id, attrs)
17
+ convert_request_boolean_attrs(attrs)
18
+
19
+ content = compose_content('user', id, attrs)
20
+
21
+ response = self.class.post(
22
+ "/user/#{id}/edit",
23
+ :body => {
24
+ :content => content
25
+ }
26
+ )
27
+
28
+ body, _ = explode_response(response)
22
29
 
23
- response = self.class.post(
24
- "/user/#{id}/edit",
25
- :body => {
26
- :content => content
30
+ case body
31
+ when /^# User (.+) updated/
32
+ id = $1
33
+ user_show(id)
34
+ when /^# You are not allowed to modify user \d+/
35
+ raise Unauthorized, body
36
+ when /^# Syntax error/
37
+ raise SyntaxError, body
38
+ else
39
+ raise UnhandledResponse
40
+ end
41
+ end
42
+
43
+ # Requires RT > 3.8.0
44
+ def user_create(attrs)
45
+ default_attrs = {
46
+ 'id' => 'user/new'
27
47
  }
28
- )
29
-
30
- body, _ = explode_response(response)
31
-
32
- case body
33
- when /^# User (.+) updated/
34
- id = body[/^# User (.+) updated/, 1]
35
- user_show(id)
36
- when /^# You are not allowed to modify user \d+/
37
- raise Unauthorized, body
38
- when /^# Syntax error/
39
- raise SyntaxError, body
40
- else
41
- raise UnhandledResponse
48
+ attrs = default_attrs.merge(attrs).stringify_keys!
49
+
50
+ content = compose_content('user', attrs['id'], attrs)
51
+
52
+ response = self.class.post(
53
+ '/user/new',
54
+ :body => {
55
+ :content => content
56
+ }
57
+ )
58
+
59
+ body, _ = explode_response(response)
60
+
61
+ case body
62
+ when /^# User (.+) created/
63
+ id = $1
64
+ # Return the whole user, not just the id.
65
+ user_show(id)
66
+ when /^# Could not create user/
67
+ raise BadRequest, body
68
+ when /^# Syntax error/
69
+ raise SyntaxError, body
70
+ else
71
+ raise UnhandledResponse, body
72
+ end
42
73
  end
43
- end
44
74
 
45
- # TODO(auxesis): add method for creating a user
75
+ alias_method :user, :user_show
76
+
77
+ private
46
78
 
47
- alias_method :user, :user_show
79
+ def convert_request_boolean_attrs(attrs)
80
+ %w(Disabled Privileged).each do |key|
81
+ if attrs.has_key?(key)
82
+ attrs[key] = case attrs[key]
83
+ when true
84
+ 1
85
+ when false
86
+ 0
87
+ end
88
+ end
89
+ end
90
+
91
+ attrs
92
+ end
93
+
94
+ def convert_response_boolean_attrs(attrs)
95
+ %w(Disabled Privileged).each do |key|
96
+ if attrs.has_key?(key)
97
+ attrs[key] = case attrs[key]
98
+ when '1'
99
+ true
100
+ when '0'
101
+ false
102
+ end
103
+ end
104
+ end
105
+
106
+ attrs
107
+ end
108
+ end
48
109
  end
data/lib/roust/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Roust
2
- VERSION = '1.4.1'
2
+ VERSION = '1.5.0'
3
3
  end
data/lib/roust.rb CHANGED
@@ -13,50 +13,61 @@ class Roust
13
13
  include Roust::User
14
14
 
15
15
  def initialize(credentials)
16
- server = credentials[:server]
17
- username = credentials[:username]
18
- password = credentials[:password]
16
+ @server = credentials[:server]
17
+ @username = credentials[:username]
18
+ @password = credentials[:password]
19
19
 
20
- if server =~ /REST\/1\.0/
21
- raise ArgumentError, "The supplied :server has REST in the URL. You only need to specify the base, e.g. http://rt.example.org/"
20
+ if @server =~ /REST\/1\.0/
21
+ raise ArgumentError, 'The supplied :server has REST in the URL. You only need to specify the base, e.g. http://rt.example.org/'
22
22
  end
23
23
 
24
- self.class.base_uri(server)
24
+ authenticate!
25
+ end
26
+
27
+ def authenticate!
28
+ # - There is no way to authenticate against the API. The only way to log
29
+ # in is to fill out the same HTML form humans fill in, cache the cookies
30
+ # returned, and send them on every subsequent request.
31
+ # - RT does not provide *any* indication that the authentication request
32
+ # has succeeded or failed. RT will always return a HTTP 200.
33
+
34
+ self.class.base_uri(@server)
25
35
 
26
36
  response = self.class.post(
27
37
  '/index.html',
28
38
  :body => {
29
- :user => username,
30
- :pass => password
39
+ :user => @username,
40
+ :pass => @password
31
41
  }
32
42
  )
33
43
 
34
- if cookie = response.headers['set-cookie']
35
- self.class.headers['Cookie'] = cookie
36
- end
44
+ cookie = response.headers['set-cookie']
45
+ self.class.headers['Cookie'] = cookie if cookie
37
46
 
38
- self.class.base_uri "#{server}/REST/1.0"
47
+ # Switch the base uri over to the actual REST API base uri.
48
+ self.class.base_uri "#{@server}/REST/1.0"
39
49
 
40
- # - There is no way to authenticate against the API. The only way to log
41
- # in is to fill out the same HTML form humans fill in, cache the cookies
42
- # returned, and send them on every subsequent request.
43
- # - RT does not provide *any* indication that the authentication request
44
- # has succeeded or failed. RT will always return a HTTP 200.
45
50
  # - The easiest way to programatically check if an authentication request
46
51
  # succeeded is by doing a request for a ticket, and seeing if the API
47
52
  # responds with some specific text ("401 Credentials required") that
48
53
  # indicates authentication has previously failed.
49
- # - The authenticated? method will raise an Unauthenticated exception if
50
- # it detects a response including this "401 Credentials required" string.
51
- authenticated?
54
+ # - The authenticated? method will return false if an Unauthenticated
55
+ # exception bubbles up from response handling. We (dirtily) rethrow the
56
+ # exception.
57
+ raise Unauthenticated unless authenticated?
52
58
  end
53
59
 
54
60
  def authenticated?
55
61
  return true if show('1')
62
+ rescue Unauthenticated
63
+ return false
56
64
  end
57
65
 
58
66
  private
59
67
 
68
+ # compose_content turns a Hash into an RFC2822 "key: value"-like header blob
69
+ #
70
+ # This is the fucked up format RT demands all content is sent and received in.
60
71
  def compose_content(type, id, attrs)
61
72
  default_attrs = {
62
73
  'id' => [ type, id ].join('/')
@@ -84,6 +95,14 @@ class Roust
84
95
  content.join("\n")
85
96
  end
86
97
 
98
+ # explode_response separates RT's response content from the response status.
99
+ #
100
+ # All HTTP-level response codes from RT are a lie. The only way to check if
101
+ # the request was successful is by inspecting the body of the content back
102
+ # from RT, and separating the first line from the rest of the content.
103
+ #
104
+ # - The first line contains the status of the operation.
105
+ # - All subsequent lines (if there are any) are the message body.
87
106
  def explode_response(response)
88
107
  body = response.body
89
108
  status = body[/RT\/\d+\.\d+\.\d+\s(\d{3}\s.*)\n/, 1]
@@ -95,4 +114,16 @@ class Roust
95
114
 
96
115
  return body, status
97
116
  end
117
+
118
+ def body_to_hash(body)
119
+ body.gsub!(/\n\s*\n/, "\n") # remove blank lines for Mail
120
+
121
+ message = Mail.new(body)
122
+ pairs = message.header.fields.map do |header|
123
+ key = header.name.to_s
124
+ value = header.value.to_s
125
+ [ key, value ]
126
+ end
127
+ hash = Hash[pairs]
128
+ end
98
129
  end
data/roust.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.authors = ['Lindsay Holmwood']
11
11
  s.email = ['lindsay@holmwood.id.au']
12
12
  s.summary = "Ruby client for RT's REST API"
13
- s.description = 'Roust is a Ruby API client that accesses the REST interface version 1.0 of a Request Tracker instance. See http://www.bestpractical.com/ for Request Tracker.'
13
+ s.description = "Roust is a Ruby API client to access Request Tracker's REST interface."
14
14
  s.homepage = 'http://github.com/bulletproofnetworks/roust'
15
15
  s.license = 'Apache 2.0'
16
16
 
@@ -0,0 +1,3 @@
1
+ RT/3.4.6 200 Ok
2
+
3
+ # User erin@us.example created.
@@ -0,0 +1,22 @@
1
+ RT/3.4.6 200 Ok
2
+
3
+ id: user/7268365
4
+ Name: erinj
5
+ Password: ********
6
+ EmailAddress: erin@us.example
7
+ RealName: Erin Jones
8
+ NickName: erin
9
+ Gecos: erin
10
+ MobilePhone: 0400123457
11
+ PagerPhone: DESC
12
+
13
+ Signature: Erin Jones
14
+ Engineering Manager
15
+ Example Org
16
+ tel: 1300 000 123
17
+ mob: 0400 123 457
18
+ fax: 02 9000 1234
19
+
20
+ Lang: en
21
+ Privileged: 0
22
+ Disabled: 0
@@ -5,12 +5,12 @@ describe Roust do
5
5
  include_context 'credentials'
6
6
 
7
7
  describe 'authentication' do
8
- it 'authenticates on instantiation' do
8
+ it 'indicates authenticated status' do
9
9
  @rt = Roust.new(credentials)
10
10
  expect(@rt.authenticated?).to eq(true)
11
11
  end
12
12
 
13
- it 'errors when credentials are incorrect' do
13
+ it 'errors on invalid credentials' do
14
14
  mocks_path = Pathname.new(__FILE__).parent.parent.join('mocks')
15
15
 
16
16
  stub_request(:post, 'http://rt.example.org/index.html')
@@ -23,8 +23,8 @@ describe Roust do
23
23
 
24
24
  describe 'queue' do
25
25
  it 'can lookup queue details' do
26
- attrs = %w(id name description correspondaddress commentaddress) +
27
- %w(initialpriority finalpriority defaultduein)
26
+ attrs = %w(id Name Description CorrespondAddress CommentAddress) +
27
+ %w(InitialPriority FinalPriority DefaultDueIn)
28
28
 
29
29
  queue = @rt.queue('13')
30
30
  attrs.each do |attr|
@@ -23,13 +23,30 @@ describe Roust do
23
23
  :body => mocks_path.join('user-dan@us.example-edit.txt').read,
24
24
  :headers => {})
25
25
 
26
+ stub_request(:post, "http://rt.example.org/REST/1.0/user/new")
27
+ .with(:body => "content=id%3A%20user%2Fnew%0AEmailAddress%3A%20erin%40us.example%0AName%3A%20erin%0ARealName%3A%20Erin%20Jones%0AGecos%3A%20erin%0ALang%3A%20en")
28
+ .to_return(:status => 200,
29
+ :body => mocks_path.join('user-erin@us.example-create.txt').read,
30
+ :headers => {})
31
+
32
+ stub_request(:get, "http://rt.example.org/REST/1.0/user/erin@us.example")
33
+ .to_return(:status => 200,
34
+ :body => mocks_path.join('user-erin@us.example.txt').read,
35
+ :headers => {})
36
+
37
+ stub_request(:post, "http://rt.example.org/REST/1.0/user/erin@us.example/edit")
38
+ .with(:body => "content=id%3A%20user%2Ferin%40us.example%0ADisabled%3A%201%0APrivileged%3A%201")
39
+ .to_return(:status => 200,
40
+ :body => "RT/3.4.6 200 Ok\n\n# User erin@us.example updated.\n",
41
+ :headers => {})
42
+
26
43
  @rt = Roust.new(credentials)
27
44
  expect(@rt.authenticated?).to eq(true)
28
45
  end
29
46
 
30
47
  describe 'user' do
31
48
  it 'can lookup user details' do
32
- attrs = %w(name realname gecos nickname emailaddress id lang password)
49
+ attrs = %w(Name RealName Gecos NickName EmailAddress id Lang Password)
33
50
 
34
51
  user = @rt.user_show('dan@us.example')
35
52
  attrs.each do |attr|
@@ -52,7 +69,40 @@ describe Roust do
52
69
  attrs = {'RealName' => 'Daniel Smith'}
53
70
  user = @rt.user_update('dan@us.example', attrs)
54
71
 
55
- expect(user['realname']).to eq('Daniel Smith')
72
+ expect(user['RealName']).to eq('Daniel Smith')
73
+ end
74
+
75
+ it 'can create a new user' do
76
+ attrs = {
77
+ 'EmailAddress' => 'erin@us.example',
78
+ 'Name' => 'erin',
79
+ 'RealName' => 'Erin Jones',
80
+ 'Gecos' => 'erin',
81
+ 'Lang' => 'en'
82
+ }
83
+ user = @rt.user_create(attrs)
84
+ expect(user['RealName']).to eq('Erin Jones')
85
+ expect(user['EmailAddress']).to eq('erin@us.example')
86
+ end
87
+
88
+ it 'does type boolean conversion on request and response' do
89
+ user = @rt.user_show('erin@us.example')
90
+ expect(user['Disabled']).to eq(false)
91
+ expect(user['Privileged']).to eq(false)
92
+
93
+ stub_request(:get, "http://rt.example.org/REST/1.0/user/erin@us.example")
94
+ .to_return(:status => 200,
95
+ :body => "RT/3.4.6 200 Ok\n\nDisabled: 1\nPrivileged: 1\n",
96
+ :headers => {})
97
+
98
+ attrs = {
99
+ 'Disabled' => true,
100
+ 'Privileged' => true,
101
+ }
102
+
103
+ user = @rt.user_update('erin@us.example', attrs)
104
+ expect(user['Disabled']).to eq(true)
105
+ expect(user['Privileged']).to eq(true)
56
106
  end
57
107
  end
58
108
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roust
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lindsay Holmwood
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-05 00:00:00.000000000 Z
11
+ date: 2014-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mail
@@ -52,8 +52,7 @@ dependencies:
52
52
  - - '>='
53
53
  - !ruby/object:Gem::Version
54
54
  version: 4.1.0
55
- description: Roust is a Ruby API client that accesses the REST interface version 1.0
56
- of a Request Tracker instance. See http://www.bestpractical.com/ for Request Tracker.
55
+ description: Roust is a Ruby API client to access Request Tracker's REST interface.
57
56
  email:
58
57
  - lindsay@holmwood.id.au
59
58
  executables: []
@@ -89,6 +88,8 @@ files:
89
88
  - spec/mocks/user-dan@us.example-after-edit.txt
90
89
  - spec/mocks/user-dan@us.example-edit.txt
91
90
  - spec/mocks/user-dan@us.example.txt
91
+ - spec/mocks/user-erin@us.example-create.txt
92
+ - spec/mocks/user-erin@us.example.txt
92
93
  - spec/mocks/user-nil.txt
93
94
  - spec/roust/authentication_spec.rb
94
95
  - spec/roust/queue_spec.rb
@@ -130,6 +131,8 @@ test_files:
130
131
  - spec/mocks/user-dan@us.example-after-edit.txt
131
132
  - spec/mocks/user-dan@us.example-edit.txt
132
133
  - spec/mocks/user-dan@us.example.txt
134
+ - spec/mocks/user-erin@us.example-create.txt
135
+ - spec/mocks/user-erin@us.example.txt
133
136
  - spec/mocks/user-nil.txt
134
137
  - spec/roust/authentication_spec.rb
135
138
  - spec/roust/queue_spec.rb