roust 1.4.1 → 1.5.0

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