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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/roust/queue.rb +13 -17
- data/lib/roust/ticket.rb +207 -211
- data/lib/roust/user.rb +101 -40
- data/lib/roust/version.rb +1 -1
- data/lib/roust.rb +51 -20
- data/roust.gemspec +1 -1
- data/spec/mocks/user-erin@us.example-create.txt +3 -0
- data/spec/mocks/user-erin@us.example.txt +22 -0
- data/spec/roust/authentication_spec.rb +2 -2
- data/spec/roust/queue_spec.rb +2 -2
- data/spec/roust/user_spec.rb +52 -2
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ecda9bb4e79252eb39c2839ff0b8ae25f4069817
|
4
|
+
data.tar.gz: fc627b8cb18996b1658249f0cf5d434a219f1a90
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5773318cc0aad6e9f4da047481c7f3aecd760854f9d94dc6cefe3a2751f0c984ca0ca141dce9eb80da50468d2dfcb1b9a1b9f3ed064353d4a8e075d94edb22fd
|
7
|
+
data.tar.gz: eaa2ffe6bbcb5ff81ffa52c759d65a3af9cc34b85c781538a536ead7494806c61eed87471edf0b875f2d6f41b686daf80dfd7ccbea76f73599d1987faa6ecc73
|
data/Gemfile.lock
CHANGED
data/lib/roust/queue.rb
CHANGED
@@ -1,21 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
15
|
+
alias_method :queue, :queue_show
|
16
|
+
end
|
21
17
|
end
|
data/lib/roust/ticket.rb
CHANGED
@@ -1,245 +1,241 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
class Roust
|
2
|
+
module Ticket
|
3
|
+
def ticket_show(id)
|
4
|
+
response = self.class.get("/ticket/#{id}/show")
|
4
5
|
|
5
|
-
|
6
|
+
body, _ = explode_response(response)
|
6
7
|
|
7
|
-
|
8
|
+
return nil if body =~ /^# (Ticket (\d+) does not exist\.)/
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
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
|
-
|
42
|
+
hash
|
43
|
+
end
|
68
44
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
:content => content
|
45
|
+
def ticket_create(attrs)
|
46
|
+
default_attrs = {
|
47
|
+
'id' => 'ticket/new'
|
73
48
|
}
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
#
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
132
|
+
def ticket_history(id, opts = {})
|
133
|
+
options = {
|
134
|
+
:format => 'short',
|
135
|
+
:comments => false
|
136
|
+
}.merge(opts)
|
142
137
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
138
|
+
format = options[:format]
|
139
|
+
comments = options[:comments]
|
140
|
+
params = {
|
141
|
+
:format => format[0]
|
142
|
+
}
|
148
143
|
|
149
|
-
|
144
|
+
response = self.class.get("/ticket/#{id}/history", :query => params)
|
150
145
|
|
151
|
-
|
146
|
+
body, _ = explode_response(response)
|
152
147
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
227
|
+
reply['attachments'] = attachments
|
231
228
|
end
|
232
|
-
|
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
|
-
|
238
|
+
list
|
239
|
+
end
|
244
240
|
end
|
245
241
|
end
|
data/lib/roust/user.rb
CHANGED
@@ -1,48 +1,109 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
75
|
+
alias_method :user, :user_show
|
76
|
+
|
77
|
+
private
|
46
78
|
|
47
|
-
|
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
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,
|
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
|
-
|
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
|
-
|
35
|
-
|
36
|
-
end
|
44
|
+
cookie = response.headers['set-cookie']
|
45
|
+
self.class.headers['Cookie'] = cookie if cookie
|
37
46
|
|
38
|
-
|
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
|
50
|
-
#
|
51
|
-
|
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 =
|
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,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 '
|
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
|
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')
|
data/spec/roust/queue_spec.rb
CHANGED
@@ -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
|
27
|
-
%w(
|
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|
|
data/spec/roust/user_spec.rb
CHANGED
@@ -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(
|
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['
|
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
|
+
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-
|
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
|
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
|