roust 1.3.0 → 1.4.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.
data/lib/roust.rb CHANGED
@@ -1,12 +1,16 @@
1
1
  require 'httparty'
2
2
  require 'mail'
3
3
  require 'active_support/core_ext/hash'
4
-
5
- class Unauthenticated < Exception ; end
4
+ require 'roust/ticket'
5
+ require 'roust/queue'
6
+ require 'roust/user'
7
+ require 'roust/exceptions'
6
8
 
7
9
  class Roust
8
10
  include HTTParty
9
- #debug_output
11
+ include Roust::Ticket
12
+ include Roust::Queue
13
+ include Roust::User
10
14
 
11
15
  def initialize(credentials)
12
16
  server = credentials[:server]
@@ -43,358 +47,48 @@ class Roust
43
47
  authenticated?
44
48
  end
45
49
 
46
- def show(id)
47
- response = self.class.get("/ticket/#{id}/show")
48
-
49
- body, status = explode_response(response)
50
-
51
- if match = body.match(/^# (Ticket (\d+) does not exist\.)/)
52
- return { 'error' => match[1] }
53
- end
54
-
55
- # Replace CF spaces with underscores
56
- while body.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/)
57
- body.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
58
- end
59
-
60
- # Sometimes the API returns requestors formatted like this:
61
- #
62
- # Requestors: foo@example.org,
63
- # bar@example.org, baz@example.org
64
- # qux@example.org, quux@example.org,
65
- # corge@example.org
66
- #
67
- # Turn it into this:
68
- #
69
- # Requestors: foo@example.org, bar@example.org, baz@example.org, ...
70
- #
71
- body.gsub!(/\n\n/, "\n")
72
-
73
- %w(Requestors Cc AdminCc).each do |field|
74
- body.gsub!(/^#{field}:(.+)^\n/m) do |match|
75
- match.strip.split(/,\s+/).join(', ').strip
76
- end
77
- end
78
-
79
- message = Mail.new(body)
80
-
81
- hash = Hash[message.header.fields.map {|header|
82
- key = header.name.to_s
83
- value = header.value.to_s
84
- [ key, value ]
85
- }]
86
-
87
- %w(Requestors Cc AdminCc).each do |field|
88
- hash[field] = hash[field].split(', ') if hash[field]
89
- end
90
-
91
- hash["id"] = hash["id"].split('/').last
92
-
93
- hash
94
- end
95
-
96
- def create(attrs)
97
- default_attrs = {
98
- 'id' => 'ticket/new'
99
- }
100
- attrs = default_attrs.merge(attrs).stringify_keys!
101
-
102
- if error = create_invalid?(attrs)
103
- return {'error' => error }
104
- end
105
-
106
- attrs['Text'].gsub!(/\n/,"\n ") if attrs['Text'] # insert a space on continuation lines.
107
-
108
- # We can't set more than one AdminCc when creating a ticket. WTF RT.
109
- #
110
- # Delete it from the ticket we are creating, and we'll update the ticket
111
- # after we've created.
112
- admincc = attrs.delete("AdminCc")
113
-
114
- content = attrs.map { |k,v|
115
- # Don't lowercase strings if they're already camel cased.
116
- k = case
117
- when k.is_a?(Symbol)
118
- k.to_s
119
- when k == 'id'
120
- k
121
- when k =~ /^[a-z]/
122
- k.capitalize
123
- else
124
- k
125
- end
126
-
127
- v = v.join(', ') if v.respond_to?(:join)
128
-
129
- "#{k}: #{v}"
130
- }.join("\n")
131
-
132
- response = self.class.post(
133
- "/ticket/new",
134
- :body => {
135
- :content => content
136
- },
137
- )
138
-
139
- body, status = explode_response(response)
140
-
141
- case body
142
- when /^# Could not create ticket/
143
- false
144
- when /^# Syntax error/
145
- false
146
- when /^# Ticket (\d+) created/
147
- id = body[/^# Ticket (\d+) created/, 1]
148
- update(id, 'AdminCc' => admincc) if admincc
149
- show(id)
150
- else
151
- # We should never hit this, but if we do, just pass it through and
152
- # surprise the user (!!!).
153
- body
154
- end
155
- end
156
-
157
- def update(id, attrs)
158
- content = compose_content('ticket', id, attrs)
159
-
160
- response = self.class.post(
161
- "/ticket/#{id}/edit",
162
- :body => {
163
- :content => content
164
- },
165
- )
166
-
167
- body, status = explode_response(response)
168
-
169
- case body
170
- when /^# You are not allowed to modify ticket \d+/
171
- { 'error' => body.strip }
172
- when /^# Syntax error/
173
- { 'error' => body.strip }
174
- when /^# Ticket (\d+) updated/
175
- id = body[/^# Ticket (\d+) updated/, 1]
176
- show(id)
177
- else
178
- # We should never hit this, but if we do, just pass it through and
179
- # surprise the user (!!!).
180
- body
181
- end
182
- end
183
-
184
50
  def authenticated?
185
51
  return true if show('1')
186
52
  end
187
53
 
188
- def search(query)
189
- params = {
190
- :query => query,
191
- :format => 's',
192
- :orderby => '+id'
193
- }
194
- response = self.class.get("/search/ticket", :query => params)
195
- body = response.body
196
- body.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"")
197
-
198
- body.split("\n").map do |t|
199
- id, subject = t.split(': ', 2)
200
- {'id' => id, 'Subject' => subject}
201
- end
202
- end
203
-
204
- def history(id, opts={})
205
- options = {
206
- :format => 'short',
207
- :comments => false
208
- }.merge(opts)
209
-
210
- format = options[:format]
211
- comments = options[:comments]
212
- params = {
213
- :format => format[0]
214
- }
215
-
216
- response = self.class.get("/ticket/#{id}/history", :query => params)
217
-
218
- body, status = explode_response(response)
219
-
220
- case format
221
- when 'short'
222
- parse_short_history(body, :comments => comments)
223
- when 'long'
224
- parse_long_history(body, :comments => comments)
225
- end
226
- end
227
-
228
- # id can be numeric (e.g. 28) or textual (e.g. sales)
229
- def queue(id)
230
- response = self.class.get("/queue/#{id}")
231
-
232
- body, status = explode_response(response)
233
- case body
234
- when /No queue named/
235
- nil
236
- else
237
- body.gsub!(/\n\s*\n/,"\n") # remove blank lines for Mail
238
- message = Mail.new(body)
239
- Hash[message.header.fields.map {|header|
240
- key = header.name.to_s.downcase
241
- value = header.value.to_s
242
- [ key, value ]
243
- }]
244
- end
245
- end
246
-
247
- # id can be numeric (e.g. 28) or textual (e.g. john)
248
- def user_show(id)
249
- response = self.class.get("/user/#{id}")
250
-
251
- body, status = explode_response(response)
252
- case body
253
- when /No user named/
254
- nil
255
- else
256
- body.gsub!(/\n\s*\n/,"\n") # remove blank lines for Mail
257
- message = Mail.new(body)
258
- Hash[message.header.fields.map {|header|
259
- key = header.name.to_s.downcase
260
- value = header.value.to_s
261
- [ key, value ]
262
- }]
263
- end
264
- end
265
-
266
- alias :user :user_show
267
-
268
- def user_update(id, attrs)
269
- content = compose_content('user', id, attrs)
270
-
271
- response = self.class.post(
272
- "/user/#{id}/edit",
273
- :body => {
274
- :content => content
275
- },
276
- )
277
-
278
- body, status = explode_response(response)
279
-
280
- case body
281
- when /^# You are not allowed to modify user \d+/
282
- { 'error' => body.strip }
283
- when /^# Syntax error/
284
- { 'error' => body.strip }
285
- when /^# User (.+) updated/
286
- id = body[/^# User (.+) updated/, 1]
287
- user_show(id)
288
- else
289
- # We should never hit this, but if we do, just pass it through and
290
- # surprise the user (!!!).
291
- body
292
- end
293
- end
294
-
295
54
  private
55
+
296
56
  def compose_content(type, id, attrs)
297
57
  default_attrs = {
298
58
  'id' => [ type, id ].join('/')
299
59
  }
300
60
  attrs = default_attrs.merge(attrs).stringify_keys!
301
61
 
302
- content = attrs.map { |k,v|
62
+ content = attrs.map do |k, v|
303
63
  # Don't lowercase strings if they're already camel cased.
304
64
  k = case
305
- when k.is_a?(Symbol)
306
- k.to_s
307
- when k == 'id'
308
- k
309
- when k =~ /^[a-z]/
310
- k.capitalize
311
- else
312
- k
313
- end
65
+ when k.is_a?(Symbol)
66
+ k.to_s
67
+ when k == 'id'
68
+ k
69
+ when k =~ /^[a-z]/
70
+ k.capitalize
71
+ else
72
+ k
73
+ end
314
74
 
315
75
  v = v.join(', ') if v.respond_to?(:join)
316
76
 
317
77
  "#{k}: #{v}"
318
- }.join("\n")
78
+ end
79
+
80
+ content.join("\n")
319
81
  end
320
82
 
321
83
  def explode_response(response)
322
84
  body = response.body
323
85
  status = body[/RT\/\d+\.\d+\.\d+\s(\d{3}\s.*)\n/, 1]
324
86
 
325
- body.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n/,"")
87
+ body.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n/, '')
326
88
  body = body.empty? ? nil : body.lstrip
327
89
 
328
- raise Unauthenticated, "Invalid username or password" if status =~ /401 Credentials required/
90
+ raise Unauthenticated, 'Invalid username or password' if status =~ /401 Credentials required/
329
91
 
330
92
  return body, status
331
93
  end
332
-
333
- def create_invalid?(attrs)
334
- missing = %w(id Subject Queue).find_all {|k| !attrs.include?(k) }
335
-
336
- if missing.empty?
337
- return false
338
- else
339
- "Needs attributes: #{missing.join(', ')}"
340
- end
341
- end
342
-
343
- def parse_short_history(body, opts={})
344
- comments = opts[:comments]
345
- regex = comments ? '^\d+:' : '^\d+: [^Comments]'
346
- history = body.split("\n").select { |l| l =~ /#{regex}/ }
347
- history.map { |l| l.split(": ", 2) }
348
- end
349
-
350
- def parse_long_history(body, opts={})
351
- comments = opts[:comments]
352
- items = body.split("\n--\n")
353
- list = []
354
- items.each do |item|
355
- # Yes, this messes with the "content:" field but that's the one that's upsetting Mail.new
356
- item.gsub!(/\n\s*\n/,"\n") # remove blank lines for Mail
357
- history = Mail.new(item)
358
- next if not comments and history['type'].to_s =~ /Comment/ # skip comments
359
- reply = {}
360
-
361
- history.header.fields.each_with_index do |header, index|
362
- next if index == 0
363
-
364
- key = header.name.to_s.downcase
365
- value = header.value.to_s
366
-
367
- attachments = []
368
- case key
369
- when "attachments"
370
- temp = item.match(/Attachments:\s*(.*)/m)
371
- if temp.class != NilClass
372
- atarr = temp[1].split("\n")
373
- atarr.map { |a| a.gsub!(/^\s*/,"") }
374
- atarr.each do |a|
375
- i = a.match(/(\d+):\s*(.*)/)
376
- s = {
377
- :id => i[1].to_s,
378
- :name => i[2].to_s
379
- }
380
- sz = i[2].match(/(.*?)\s*\((.*?)\)/)
381
- if sz.class == MatchData
382
- s[:name] = sz[1].to_s
383
- s[:size] = sz[2].to_s
384
- end
385
- attachments << s
386
- end
387
- reply["attachments"] = attachments
388
- end
389
- when "content"
390
- reply["content"] = value
391
- else
392
- reply["#{key}"] = value
393
- end
394
- end
395
- list << reply
396
- end
397
-
398
- return list
399
- end
400
94
  end
data/roust.gemspec CHANGED
@@ -1,25 +1,25 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "roust/version"
2
+ $LOAD_PATH.push(File.expand_path('../lib', __FILE__))
3
+ require 'roust/version'
4
4
 
5
5
  Gem::Specification.new do |s|
6
- s.name = "roust"
6
+ s.name = 'roust'
7
7
  s.version = Roust::VERSION
8
- s.date = %q{2014-01-23}
8
+ s.date = Time.now.strftime('%Y-%m-%d')
9
9
 
10
- s.authors = [ "Lindsay Holmwood" ]
11
- s.email = [ "lindsay@holmwood.id.au" ]
12
- s.summary = %q{Ruby client for RT's REST API}
13
- s.description = %q{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.}
14
- s.homepage = "http://github.com/bulletproofnetworks/roust"
15
- s.license = "Apache 2.0"
10
+ s.authors = ['Lindsay Holmwood']
11
+ s.email = ['lindsay@holmwood.id.au']
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.'
14
+ s.homepage = 'http://github.com/bulletproofnetworks/roust'
15
+ s.license = 'Apache 2.0'
16
16
 
17
- s.required_ruby_version = ">= 1.9.2"
17
+ s.required_ruby_version = '>= 1.9.2'
18
18
 
19
19
  s.files = `git ls-files`.split("\n")
20
20
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
- s.require_paths = ["lib"]
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
22
+ s.require_paths = %w(lib)
23
23
 
24
24
  s.add_runtime_dependency 'mail', '>= 2.5.4'
25
25
  s.add_runtime_dependency 'httparty', '>= 0.13.1'
@@ -0,0 +1 @@
1
+ RT/3.4.6 401 Credentials required
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'roust'
3
+
4
+ describe Roust do
5
+ include_context 'credentials'
6
+
7
+ describe 'authentication' do
8
+ it 'authenticates on instantiation' do
9
+ @rt = Roust.new(credentials)
10
+ expect(@rt.authenticated?).to eq(true)
11
+ end
12
+
13
+ it 'errors when credentials are incorrect' do
14
+ mocks_path = Pathname.new(__FILE__).parent.parent.join('mocks')
15
+
16
+ stub_request(:post, 'http://rt.example.org/index.html').
17
+ with(:body => {
18
+ 'user'=>'admin',
19
+ 'pass'=>'incorrect',
20
+ }).
21
+ to_return(:status => 200, :body => '', :headers => {})
22
+
23
+ stub_request(:get, 'http://rt.example.org/REST/1.0/ticket/1/show').
24
+ to_return(:status => 200,
25
+ :body => mocks_path.join('ticket-1-show-unauthenticated.txt').read,
26
+ :headers => {})
27
+
28
+ credentials.merge!({:username => 'admin', :password => 'incorrect'})
29
+
30
+ expect { Roust.new(credentials) }.to raise_error(Unauthenticated)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+ require 'roust'
3
+
4
+ describe Roust do
5
+ include_context 'credentials'
6
+
7
+ before do
8
+ mocks_path = Pathname.new(__FILE__).parent.parent.join('mocks')
9
+
10
+ stub_request(:get, 'http://rt.example.org/REST/1.0/queue/13')
11
+ .to_return(:status => 200,
12
+ :body => mocks_path.join('queue-13.txt').read,
13
+ :headers => {})
14
+
15
+ stub_request(:get, 'http://rt.example.org/REST/1.0/queue/nil')
16
+ .to_return(:status => 200,
17
+ :body => mocks_path.join('queue-nil.txt').read,
18
+ :headers => {})
19
+
20
+ @rt = Roust.new(credentials)
21
+ expect(@rt.authenticated?).to eq(true)
22
+ end
23
+
24
+ describe 'queue' do
25
+ it 'can lookup queue details' do
26
+ attrs = %w(id name description correspondaddress commentaddress) +
27
+ %w(initialpriority finalpriority defaultduein)
28
+
29
+ queue = @rt.queue('13')
30
+ attrs.each do |attr|
31
+ expect(queue[attr]).to_not eq(nil), "#{attr} key doesn't exist"
32
+ end
33
+ end
34
+
35
+ it 'returns nil for unknown queues' do
36
+ queue = @rt.queue('nil')
37
+ expect(queue).to eq(nil)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+ require 'roust'
3
+
4
+ describe Roust do
5
+ include_context 'credentials'
6
+
7
+ before do
8
+ mocks_path = Pathname.new(__FILE__).parent.parent.join('mocks')
9
+
10
+ stub_request(:get, 'http://rt.example.org/REST/1.0/search/ticket?format=s&orderby=%2Bid&query%5Bquery%5D=id%20=%201%20or%20id%20=%202')
11
+ .to_return(:status => 200,
12
+ :body => mocks_path.join('ticket-search-1-or-2.txt').read,
13
+ :headers => {})
14
+
15
+ stub_request(:get, 'http://rt.example.org/REST/1.0/ticket/1/history?format=s')
16
+ .to_return(:status => 200,
17
+ :body => mocks_path.join('ticket-1-history-short.txt').read,
18
+ :headers => {})
19
+
20
+ stub_request(:get, 'http://rt.example.org/REST/1.0/ticket/1/history?format=l')
21
+ .to_return(:status => 200,
22
+ :body => mocks_path.join('ticket-1-history-long.txt').read,
23
+ :headers => {})
24
+
25
+ @rt = Roust.new(credentials)
26
+ expect(@rt.authenticated?).to eq(true)
27
+ end
28
+
29
+ describe 'tickets' do
30
+ it 'can list tickets matching a query' do
31
+ results = @rt.search(:query => 'id = 1 or id = 2')
32
+ expect(results.size).to eq(2)
33
+ results.each do |result|
34
+ expect(result.size).to eq(2)
35
+ end
36
+ end
37
+
38
+ it 'can fetch metadata on individual tickets' do
39
+ ticket = @rt.show('1')
40
+ expect(ticket).to_not eq(nil)
41
+
42
+ attrs = %w(id Subject Queue) +
43
+ %w(Requestors Cc AdminCc Owner Creator) +
44
+ %w(Resolved Status) +
45
+ %w(Starts Started TimeLeft Due TimeWorked TimeEstimated) +
46
+ %w(LastUpdated Created Told) +
47
+ %w(Priority FinalPriority InitialPriority)
48
+
49
+ attrs.each do |attr|
50
+ expect(ticket[attr]).to_not eq(nil), "#{attr} key doesn't exist"
51
+ end
52
+
53
+ %w(Requestors Cc AdminCc).each do |field|
54
+ expect(ticket[field].size).to be > 1
55
+ end
56
+ end
57
+
58
+ it 'can fetch transactions on individual tickets' do
59
+ short = @rt.history('1', :format => 'short')
60
+
61
+ expect(short.size).to be > 1
62
+ short.each do |txn|
63
+ expect(txn.size).to eq(2)
64
+ expect(txn.first).to match(/^\d+$/)
65
+ expect(txn.last).to match(/^\w.*\w$/)
66
+ end
67
+
68
+ attrs = %w(ticket data oldvalue timetaken) +
69
+ %w(id type field newvalue content description)
70
+
71
+ long = @rt.history('1', :format => 'long')
72
+ expect(long.size).to be > 0
73
+ long.each do |txn|
74
+ attrs.each do |attr|
75
+ expect(txn[attr]).to_not eq(nil), "#{attr} key doesn't exist"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+ require 'roust'
3
+
4
+ describe Roust do
5
+ include_context 'credentials'
6
+
7
+ before do
8
+ mocks_path = Pathname.new(__FILE__).parent.parent.join('mocks')
9
+
10
+ stub_request(:get, 'http://rt.example.org/REST/1.0/user/dan@us.example')
11
+ .to_return(:status => 200,
12
+ :body => mocks_path.join('user-dan@us.example.txt').read,
13
+ :headers => {})
14
+
15
+ stub_request(:get, 'http://rt.example.org/REST/1.0/user/nil')
16
+ .to_return(:status => 200,
17
+ :body => mocks_path.join('user-nil.txt').read,
18
+ :headers => {})
19
+
20
+ stub_request(:post, 'http://rt.example.org/REST/1.0/user/dan@us.example/edit')
21
+ .with(:body => 'content=id%3A%20user%2Fdan%40us.example%0ARealName%3A%20Daniel%20Smith')
22
+ .to_return(:status => 200,
23
+ :body => mocks_path.join('user-dan@us.example-edit.txt').read,
24
+ :headers => {})
25
+
26
+ @rt = Roust.new(credentials)
27
+ expect(@rt.authenticated?).to eq(true)
28
+ end
29
+
30
+ describe 'user' do
31
+ it 'can lookup user details' do
32
+ attrs = %w(name realname gecos nickname emailaddress id lang password)
33
+
34
+ user = @rt.user_show('dan@us.example')
35
+ attrs.each do |attr|
36
+ expect(user[attr]).to_not eq(nil), "#{attr} key doesn't exist"
37
+ end
38
+ end
39
+
40
+ it 'returns nil for unknown users' do
41
+ queue = @rt.user_show('nil')
42
+ expect(queue).to eq(nil)
43
+ end
44
+
45
+ it 'can modify an existing user' do
46
+ mocks_path = Pathname.new(__FILE__).parent.parent.join('mocks')
47
+ stub_request(:get, 'http://rt.example.org/REST/1.0/user/dan@us.example')
48
+ .to_return(:status => 200,
49
+ :body => mocks_path.join('user-dan@us.example-after-edit.txt').read,
50
+ :headers => {})
51
+
52
+ attrs = {'RealName' => 'Daniel Smith'}
53
+ user = @rt.user_update('dan@us.example', attrs)
54
+
55
+ expect(user['realname']).to eq('Daniel Smith')
56
+ end
57
+ end
58
+ end
data/spec/spec_helper.rb CHANGED
@@ -7,20 +7,39 @@
7
7
 
8
8
  require 'pathname'
9
9
  lib = Pathname.new(__FILE__).parent.parent.join('lib').to_s
10
- $: << lib
10
+ $LOAD_PATH << lib
11
11
  require 'webmock/rspec'
12
12
 
13
13
  RSpec.configure do |config|
14
- # Use color in STDOUT
15
- config.color_enabled = true
16
-
17
14
  # Use color not only in STDOUT but also in pagers and files
18
15
  config.tty = true
16
+ end
17
+
18
+ # Boilerplate for all tests.
19
+ #
20
+ # All tests need to authenticate before they can do anything, so mock it out.
21
+ RSpec.shared_context 'credentials' do
22
+ let :credentials do
23
+ {
24
+ :server => 'http://rt.example.org',
25
+ :username => 'admin',
26
+ :password => 'password'
27
+ }
28
+ end
19
29
 
20
- # Use the specified formatter
21
- config.formatter = :documentation # :progress, :html, :textmate
30
+ before(:each) do
31
+ mocks_path = Pathname.new(__FILE__).parent.join('mocks')
22
32
 
23
- # Rspec 3 forward compatibility
24
- config.treat_symbols_as_metadata_keys_with_true_values = true
25
- end
33
+ stub_request(:post, 'http://rt.example.org/index.html')
34
+ .with(:body => {
35
+ 'user' => 'admin',
36
+ 'pass' => 'password'
37
+ })
38
+ .to_return(:status => 200, :body => '', :headers => {})
26
39
 
40
+ stub_request(:get, 'http://rt.example.org/REST/1.0/ticket/1/show')
41
+ .to_return(:status => 200,
42
+ :body => mocks_path.join('ticket-1-show.txt').read,
43
+ :headers => {})
44
+ end
45
+ end