4me-sdk 1.1.5 → 2.0.0.pre.rc.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,7 +8,7 @@ require 'mime/types'
8
8
  # Created:: 22 Feb 2008
9
9
  module Sdk4me
10
10
  module Multipart
11
- VERSION = "1.0.0" unless const_defined?(:VERSION)
11
+ VERSION = '1.0.0'.freeze unless const_defined?(:VERSION)
12
12
 
13
13
  # Formats a given hash as a multipart form post
14
14
  # If a hash value responds to :string or :read messages, then it is
@@ -16,10 +16,10 @@ module Sdk4me
16
16
  # to be a string
17
17
  class Post
18
18
  # We have to pretend like we're a web browser...
19
- USERAGENT = "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6" unless const_defined?(:USERAGENT)
20
- BOUNDARY = '0123456789ABLEWASIEREISAWELBA9876543210' unless const_defined?(:BOUNDARY)
21
- CONTENT_TYPE = "multipart/form-data; boundary=#{ BOUNDARY }" unless const_defined?(:CONTENT_TYPE)
22
- HEADER = { 'Content-Type' => CONTENT_TYPE, 'User-Agent' => USERAGENT } unless const_defined?(:HEADER)
19
+ USERAGENT = "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6 4me/#{Sdk4me::Client::VERSION}".freeze unless const_defined?(:USERAGENT)
20
+ BOUNDARY = '0123456789ABLEWASIEREISAWELBA9876543210'.freeze unless const_defined?(:BOUNDARY)
21
+ CONTENT_TYPE = "multipart/form-data; boundary=#{BOUNDARY}".freeze unless const_defined?(:CONTENT_TYPE)
22
+ HEADER = { 'Content-Type' => CONTENT_TYPE, 'User-Agent' => USERAGENT }.freeze unless const_defined?(:HEADER)
23
23
 
24
24
  def self.prepare_query(params)
25
25
  fp = []
@@ -33,24 +33,22 @@ module Sdk4me
33
33
  end
34
34
 
35
35
  # Assemble the request body using the special multipart format
36
- query = fp.map{ |p| "--#{BOUNDARY}\r\n#{p.to_multipart}" }.join + "--#{BOUNDARY}--"
37
- return query, HEADER
36
+ query = fp.map { |p| "--#{BOUNDARY}\r\n#{p.to_multipart}" }.join + "--#{BOUNDARY}--"
37
+ [query, HEADER]
38
38
  end
39
39
  end
40
40
 
41
- private
42
-
43
41
  # Formats a basic string key/value pair for inclusion with a multipart post
44
42
  class StringParam
45
43
  attr_accessor :k, :v
46
44
 
47
- def initialize(k, v)
48
- @k = k
49
- @v = v
45
+ def initialize(key, value)
46
+ @k = key
47
+ @v = value
50
48
  end
51
49
 
52
50
  def to_multipart
53
- return %(Content-Disposition: form-data; name="#{CGI::escape(k.to_s)}"\r\n\r\n#{v}\r\n)
51
+ %(Content-Disposition: form-data; name="#{CGI.escape(k.to_s)}"\r\n\r\n#{v}\r\n)
54
52
  end
55
53
  end
56
54
 
@@ -59,17 +57,17 @@ module Sdk4me
59
57
  class FileParam
60
58
  attr_accessor :k, :filename, :content
61
59
 
62
- def initialize(k, filename, content)
63
- @k = k
60
+ def initialize(key, filename, content)
61
+ @k = key
64
62
  @filename = filename
65
63
  @content = content
66
64
  end
67
65
 
68
66
  def to_multipart
69
67
  # If we can tell the possible mime-type from the filename, use the first in the list; otherwise, use "application/octet-stream"
70
- mime_type = MIME::Types.type_for(filename)[0] || MIME::Types["application/octet-stream"][0]
71
- return %(Content-Disposition: form-data; name="#{CGI::escape(k.to_s)}"; filename="#{filename}"\r\nContent-Type: #{ mime_type.simplified }\r\n\r\n#{ content }\r\n)
68
+ mime_type = MIME::Types.type_for(filename)[0] || MIME::Types['application/octet-stream'][0]
69
+ %(Content-Disposition: form-data; name="#{CGI.escape(k.to_s)}"; filename="#{filename}"\r\nContent-Type: #{mime_type.simplified}\r\n\r\n#{content}\r\n)
72
70
  end
73
71
  end
74
72
  end
75
- end
73
+ end
@@ -1,19 +1,13 @@
1
1
  module Sdk4me
2
2
  class Response
3
+ attr_reader :request, :response
4
+ alias raw response
5
+
3
6
  def initialize(request, response)
4
7
  @request = request
5
8
  @response = response
6
9
  end
7
10
 
8
- def request
9
- @request
10
- end
11
-
12
- def response
13
- @response
14
- end
15
- alias_method :raw, :response
16
-
17
11
  def body
18
12
  @response.body
19
13
  end
@@ -22,16 +16,17 @@ module Sdk4me
22
16
  # If the response is not +valid?+ it is a Hash with 'message' and optionally 'errors'
23
17
  def json
24
18
  return @json if defined?(@json)
19
+
25
20
  # no content, no JSON
26
21
  if @response.code.to_s == '204'
27
22
  data = {}
28
23
  elsif @response.body.blank?
29
24
  # no body, no json
30
- data = {message: @response.message.blank? ? 'empty body' : @response.message.strip}
25
+ data = { message: @response.message.blank? ? 'empty body' : @response.message.strip }
31
26
  end
32
27
  begin
33
28
  data ||= JSON.parse(@response.body)
34
- rescue ::Exception => e
29
+ rescue StandardError => e
35
30
  data = { message: "Invalid JSON - #{e.message} for:\n#{@response.body}" }
36
31
  end
37
32
  # indifferent access to hashes
@@ -57,22 +52,31 @@ module Sdk4me
57
52
  def valid?
58
53
  message.nil?
59
54
  end
60
- alias_method :success?, :valid?
55
+ alias success? valid?
56
+
57
+ # +true+ in case of a HTTP 5xx error
58
+ def failure?
59
+ !success? && (@response.code.to_s.blank? || @response.code.to_s =~ /5\d\d/)
60
+ end
61
61
 
62
62
  # retrieve a value from the resource
63
63
  # if the JSON value is an Array a array with the value for each resource will be given
64
64
  # @param keys: a single key or a key-path separated by comma
65
65
  def[](*keys)
66
66
  values = json.is_a?(Array) ? json : [json]
67
- keys.each { |key| values = values.map{ |value| value.is_a?(Hash) ? value[key] : nil} }
67
+ keys.each { |key| values = values.map { |value| value.is_a?(Hash) ? value[key] : nil } }
68
68
  json.is_a?(Array) ? values : values.first
69
69
  end
70
70
 
71
71
  # The nr of resources found
72
72
  def size
73
- @size ||= message ? 0 : json.is_a?(Array) ? json.size : 1
73
+ @size ||= if message
74
+ 0
75
+ else
76
+ json.is_a?(Array) ? json.size : 1
77
+ end
74
78
  end
75
- alias :count :size
79
+ alias count size
76
80
 
77
81
  # pagination - per page
78
82
  def per_page
@@ -98,12 +102,12 @@ module Sdk4me
98
102
  # Link: <https://api.4me.com/v1/requests?page=1&per_page=25>; rel="first", <https://api.4me.com/v1/requests?page=2&per_page=25>; rel="prev", etc.
99
103
  def pagination_link(relation)
100
104
  # split on ',' select the [url] in '<[url]>; rel="[relation]"', compact to all url's found (at most one) and take the first
101
- (@pagination_links ||= {})[relation] ||= @response.header['Link'] && @response.header['Link'].split(/,\s*<?/).map{ |link| link[/^\s*<?(.*?)>?;\s*rel="#{relation.to_s}"\s*$/, 1] }.compact.first
105
+ (@pagination_links ||= {})[relation] ||= @response.header['Link'] && @response.header['Link'].split(/,\s*<?/).map { |link| link[/^\s*<?(.*?)>?;\s*rel="#{relation}"\s*$/, 1] }.compact.first
102
106
  end
103
107
 
104
108
  # pagination urls (relative paths without server) - relations :first, :prev, :next, :last
105
109
  def pagination_relative_link(relation)
106
- (@pagination_relative_links ||= {})[relation] ||= pagination_link(relation) && pagination_link(relation)[/^https?:\/\/[^\/]*(.*)/, 1]
110
+ (@pagination_relative_links ||= {})[relation] ||= pagination_link(relation) && pagination_link(relation)[%r{^https?://[^/]*(.*)}, 1]
107
111
  end
108
112
 
109
113
  # +true+ if the response is invalid because of throttling
@@ -112,12 +116,11 @@ module Sdk4me
112
116
  end
113
117
 
114
118
  def retry_after
115
- @current_page ||= @response.header['Retry-After'].to_i
119
+ @retry_after ||= @response.header['Retry-After'].to_i
116
120
  end
117
121
 
118
122
  def to_s
119
123
  valid? ? json.to_s : message
120
124
  end
121
-
122
125
  end
123
126
  end
@@ -1,5 +1,5 @@
1
1
  module Sdk4me
2
2
  class Client
3
- VERSION = '1.1.5'
3
+ VERSION = '2.0.0-rc.1'.freeze
4
4
  end
5
5
  end
@@ -1,178 +1,342 @@
1
1
  require 'spec_helper'
2
+ require 'tempfile'
2
3
 
3
4
  describe Sdk4me::Attachments do
4
-
5
- before(:each) do
6
- @client = Sdk4me::Client.new(api_token: 'secret', max_retry_time: -1)
7
- @attachments = Sdk4me::Attachments.new(@client)
5
+ def attachments(authentication, path)
6
+ @client = if authentication == :api_token
7
+ Sdk4me::Client.new(api_token: 'secret', max_retry_time: -1)
8
+ else
9
+ Sdk4me::Client.new(access_token: 'secret', max_retry_time: -1)
10
+ end
11
+ Sdk4me::Attachments.new(@client, path)
8
12
  end
9
13
 
10
- context 'upload_attachments!' do
11
- it 'should not do anything when no :attachments are present' do
12
- expect(@attachments.upload_attachments!('/requests', {status: :in_progress})).to be_nil
14
+ def credentials(authentication)
15
+ if authentication == :api_token
16
+ { basic_auth: %w[secret x] }
17
+ else
18
+ { headers: { 'Authorization' => 'Bearer secret' } }
13
19
  end
20
+ end
14
21
 
15
- it 'should not do anything when :attachments is nil' do
16
- expect(@attachments.upload_attachments!('/requests', {attachments: nil})).to be_nil
17
- end
22
+ %i[api_token access_token].each do |authentication|
23
+ context "#{authentication} -" do
24
+ context 'upload_attachments! -' do
25
+ context 'field attachments' do
26
+ it 'should not do anything when no attachments are present' do
27
+ a = attachments(authentication, '/requests')
28
+ expect(@client).not_to receive(:send_file)
29
+ a.upload_attachments!({ status: :in_progress })
30
+ end
18
31
 
19
- it 'should not do anything when :attachments is empty' do
20
- expect(@attachments.upload_attachments!('/requests', {attachments: []})).to be_nil
21
- expect(@attachments.upload_attachments!('/requests', {attachments: [nil]})).to be_nil
22
- end
32
+ it 'should not do anything when attachments is nil' do
33
+ a = attachments(authentication, '/requests')
34
+ expect(@client).not_to receive(:send_file)
35
+ a.upload_attachments!({ note_attachments: nil })
36
+ end
23
37
 
24
- it 'should show a error if no attachment may be uploaded' do
25
- stub_request(:get, 'https://api.4me.com/v1/sites/1?attachment_upload_token=true').with(basic_auth: ['secret', 'x']).to_return(body: {name: 'site 1'}.to_json)
26
- expect_log('Attachments not allowed for /sites/1', :error)
27
- expect(@attachments.upload_attachments!('/sites/1', {attachments: ['file1.png']})).to be_nil
28
- end
38
+ it 'should not do anything when attachments is empty' do
39
+ a = attachments(authentication, '/requests')
40
+ expect(@client).not_to receive(:send_file)
41
+ a.upload_attachments!({ note_attachments: [] })
42
+ a.upload_attachments!({ note_attachments: [nil] })
43
+ end
29
44
 
30
- it 'should raise an exception if no attachment may be uploaded' do
31
- stub_request(:get, 'https://api.4me.com/v1/sites/1?attachment_upload_token=true').with(basic_auth: ['secret', 'x']).to_return(body: {name: 'site 1'}.to_json)
32
- message = 'Attachments not allowed for /sites/1'
33
- expect{ @attachments.upload_attachments!('/sites/1', {attachments: ['file1.png'], attachments_exception: true}) }.to raise_error(::Sdk4me::UploadFailed, message)
34
- end
45
+ it 'should raise an error if no attachment provider can be determined' do
46
+ a = attachments(authentication, '/requests')
47
+ expect(@client).not_to receive(:send_file)
48
+ stub_request(:get, 'https://api.4me.com/v1/attachments/storage').with(credentials(authentication)).to_return(status: 404, body: { message: 'Not Found' }.to_json)
49
+ expect_log('GET request to api.4me.com:443/v1/attachments/storage failed: 404: Not Found', :error)
50
+ expect_log('Attachment upload failed: No provider found', :error)
51
+ expect { a.upload_attachments!({ note_attachments: ['file1.png'] }) }.to raise_error(::Sdk4me::UploadFailed, 'Attachment upload failed: No provider found')
52
+ end
35
53
 
36
- it 'should add /new to the path for new records' do
37
- stub_request(:get, 'https://api.4me.com/v1/sites/new?attachment_upload_token=true').with(basic_auth: ['secret', 'x']).to_return(body: {missing: 'storage'}.to_json)
38
- expect_log('Attachments not allowed for /sites', :error)
39
- expect(@attachments.upload_attachments!('/sites', {attachments: ['file1.png']})).to be_nil
40
- end
54
+ it 'should upload' do
55
+ a = attachments(authentication, '/requests')
56
+ resp = {
57
+ provider: 'local',
58
+ upload_uri: 'https://widget.example.com/attachments',
59
+ local: {
60
+ key: 'attachments/5/requests/000/000/777/abc/${filename}',
61
+ x_4me_expiration: '2020-11-01T23:59:59Z',
62
+ x_4me_signature: 'foobar'
63
+ }
64
+ }
65
+ stub_request(:get, 'https://api.4me.com/v1/attachments/storage').with(credentials(authentication)).to_return(status: 200, body: resp.to_json)
41
66
 
42
- [ [:requests, :note],
43
- [:problems, :note],
44
- [:contracts, :remarks],
45
- [:cis, :remarks],
46
- [:flsas, :remarks],
47
- [:slas, :remarks],
48
- [:service_instances, :remarks],
49
- [:service_offerings, :summary],
50
- [:any_other_model, :note]].each do |model, attribute|
51
-
52
- it "should replace :attachments with :#{attribute}_attachments after upload at /#{model}" do
53
- stub_request(:get, "https://api.4me.com/v1/#{model}/new?attachment_upload_token=true").with(basic_auth: ['secret', 'x']).to_return(body: {storage_upload: 'conf'}.to_json)
54
- expect(@attachments).to receive(:upload_attachment).with('conf', 'file1.png', false).ordered{ 'uploaded file1.png' }
55
- expect(@attachments).to receive(:upload_attachment).with('conf', 'file2.zip', false).ordered{ 'uploaded file2.zip' }
56
- data = {leave: 'me alone', attachments: %w(file1.png file2.zip)}
57
- @attachments.upload_attachments!("/#{model}", data)
58
- expect(data[:attachments]).to be_nil
59
- expect(data[:leave]).to eq('me alone')
60
- expect(data[:"#{attribute}_attachments"]).to eq(['uploaded file1.png', 'uploaded file2.zip'].to_json)
61
- end
62
- end
67
+ expect(a).to(receive(:upload_attachment).with('/tmp/file1.png').ordered { { key: 'attachments/5/requests/000/000/777/abc/file1.png', filesize: 1234 } })
68
+ expect(a).to(receive(:upload_attachment).with('/tmp/file2.zip').ordered { { key: 'attachments/5/requests/000/000/777/abc/file2.zip', filesize: 9876 } })
63
69
 
64
- it 'should set raise_exception flag to true when :attachments_exception is set' do
65
- stub_request(:get, 'https://api.4me.com/v1/requests/new?attachment_upload_token=true').with(basic_auth: ['secret', 'x']).to_return(body: {storage_upload: 'conf'}.to_json)
66
- expect(@attachments).to receive(:upload_attachment).with('conf', 'file1.png', true).ordered{ 'uploaded file1.png' }
67
- data = {leave: 'me alone', attachments: 'file1.png', attachments_exception: true}
68
- @attachments.upload_attachments!('/requests', data)
69
- expect(data[:attachments]).to be_nil
70
- expect(data[:attachments_exception]).to be_nil
71
- expect(data[:leave]).to eq('me alone')
72
- expect(data[:note_attachments]).to eq(['uploaded file1.png'].to_json)
73
- end
74
- end
70
+ data = { subject: 'Foobar', note_attachments: ['/tmp/file1.png', '/tmp/file2.zip'] }
71
+ a.upload_attachments!(data)
72
+ expect(data).to eq({ subject: 'Foobar', note_attachments: [
73
+ { filesize: 1234, key: 'attachments/5/requests/000/000/777/abc/file1.png' },
74
+ { filesize: 9876, key: 'attachments/5/requests/000/000/777/abc/file2.zip' }
75
+ ] })
76
+ end
77
+ end
75
78
 
76
- context 'upload_attachment' do
79
+ context 'rich text inline attachments' do
80
+ it 'should not do anything when no [note_attachments: <idx>] is present in the note' do
81
+ a = attachments(authentication, '/requests')
82
+ expect(@client).not_to receive(:send_file)
83
+ data = { note: '[note_attachments: foo]' }
84
+ a.upload_attachments!(data)
85
+ expect(data).to eq({ note: '[note_attachments: foo]' })
86
+ end
77
87
 
78
- it 'should log an exception when the file could not be found' do
79
- expect_log('Attachment upload failed: file does not exist: unknown_file', :error)
80
- expect(@attachments.send(:upload_attachment, nil, 'unknown_file', false)).to be_nil
81
- end
88
+ it 'should not do anything when note attachments is empty' do
89
+ a = attachments(authentication, '/requests')
90
+ expect(@client).not_to receive(:send_file)
91
+ data = { note: '[note_attachments: 0]' }
92
+ a.upload_attachments!(data)
93
+ expect(data).to eq({ note: '[note_attachments: 0]' })
94
+ end
82
95
 
83
- it 'should raise an exception when the file could not be found' do
84
- message = 'Attachment upload failed: file does not exist: unknown_file'
85
- expect{ @attachments.send(:upload_attachment, nil, 'unknown_file', true) }.to raise_error(::Sdk4me::UploadFailed, message)
86
- end
96
+ it 'should raise an error if no attachment provider can be determined' do
97
+ a = attachments(authentication, '/requests')
98
+ expect(@client).not_to receive(:send_file)
99
+ stub_request(:get, 'https://api.4me.com/v1/attachments/storage').with(credentials(authentication)).to_return(status: 404, body: { message: 'Not Found' }.to_json)
100
+ expect_log('GET request to api.4me.com:443/v1/attachments/storage failed: 404: Not Found', :error)
101
+ expect_log('Attachment upload failed: No provider found', :error)
102
+ data = {
103
+ note: '[note_attachments: 0]', note_attachments: ['/tmp/doesnotexist.log']
104
+ }
105
+ expect { a.upload_attachments!(data) }.to raise_error(::Sdk4me::UploadFailed, 'Attachment upload failed: No provider found')
106
+ end
87
107
 
88
- context 'aws' do
89
- before(:each) do
90
- @aws_conf = {
91
- provider: 'aws',
92
- upload_uri: 'https://itrp.s3.amazonaws.com/',
93
- access_key: 'AKIA6RYQ',
94
- success_url: 'https://mycompany.4me.com/s3_success?sig=99e82e8a046',
95
- policy: 'eydlgIH0=',
96
- signature: 'nbhdec4k=',
97
- upload_path: 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/'
98
- }
99
- @key_template = 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}'
100
- @key = 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/upload.txt'
101
-
102
- @multi_part_body = "--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"Content-Type\"\r\n\r\napplication/octet-stream\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x-amz-server-side-encryption\"\r\n\r\nAES256\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\nattachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"AWSAccessKeyId\"\r\n\r\nAKIA6RYQ\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"acl\"\r\n\r\nprivate\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"signature\"\r\n\r\nnbhdec4k=\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"success_action_status\"\r\n\r\n201\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"policy\"\r\n\r\neydlgIH0=\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"file\"; filename=\"#{@fixture_dir}/upload.txt\"\r\nContent-Type: text/plain\r\n\r\ncontent\r\n--0123456789ABLEWASIEREISAWELBA9876543210--"
103
- @multi_part_headers = {'Accept'=>'*/*', 'Content-Type'=>'multipart/form-data; boundary=0123456789ABLEWASIEREISAWELBA9876543210', 'User-Agent'=>'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6'}
104
- end
108
+ it 'should upload' do
109
+ a = attachments(authentication, '/requests')
110
+ resp = {
111
+ provider: 'local',
112
+ upload_uri: 'https://widget.example.com/attachments',
113
+ local: {
114
+ key: 'attachments/5/requests/000/000/777/abc/${filename}',
115
+ x_4me_expiration: '2020-11-01T23:59:59Z',
116
+ x_4me_signature: 'foobar'
117
+ }
118
+ }
119
+ stub_request(:get, 'https://api.4me.com/v1/attachments/storage').with(credentials(authentication)).to_return(status: 200, body: resp.to_json)
105
120
 
106
- it 'should open a file from disk' do
107
- expect(@attachments).to receive(:aws_upload).with(@aws_conf, @key_template, @key, kind_of(File))
108
- expect(@attachments.send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", false)).to eq({key: @key, filesize: 7})
109
- end
121
+ expect(a).to(receive(:upload_attachment).with('/tmp/file1.png').ordered { { key: 'attachments/5/requests/000/000/777/abc/file1.png', filesize: 1234 } })
122
+ expect(a).to(receive(:upload_attachment).with('/tmp/file2.jpg').ordered { { key: 'attachments/5/requests/000/000/777/abc/file2.jpg', filesize: 9876 } })
110
123
 
111
- it 'should sent the upload to AWS' do
112
- stub_request(:post, 'https://itrp.s3.amazonaws.com/').with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: 'OK', status: 303, headers: {'Location' => 'https://mycompany.4me.com/s3_success?sig=99e82e8a046'})
113
- stub_request(:get, "https://api.4me.com/v1/s3_success?sig=99e82e8a046&key=#{@key}").with(basic_auth: ['secret', 'x']).to_return(body: {}.to_json)
114
- expect(@attachments.send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", false)).to eq({key: @key, filesize: 7})
115
- end
124
+ data = {
125
+ subject: 'Foobar',
126
+ note: 'Foo [note_attachments: 0] Bar [note_attachments: 1]',
127
+ note_attachments: ['/tmp/file1.png', '/tmp/file2.jpg']
128
+ }
129
+ a.upload_attachments!(data)
116
130
 
117
- it 'should report an error when AWS upload fails' do
118
- stub_request(:post, 'https://itrp.s3.amazonaws.com/').with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: %(<?xml version="1.0" encoding="UTF-8"?>\n<Error><Code>AccessDenied</Code><Message>Invalid according to Policy</Message><RequestId>1FECC4B719E426B1</RequestId><HostId>15+14lXt+HlF</HostId></Error>), status: 303, headers: {'Location' => 'https://mycompany.4me.com/s3_success?sig=99e82e8a046'})
119
- expect_log("Attachment upload failed: AWS upload to https://itrp.s3.amazonaws.com/ for #{@key} failed: Invalid according to Policy", :error)
120
- expect(@attachments.send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", false)).to be_nil
121
- end
131
+ expect(data).to eq({
132
+ note: 'Foo ![](attachments/5/requests/000/000/777/abc/file1.png) Bar ![](attachments/5/requests/000/000/777/abc/file2.jpg)',
133
+ note_attachments: [
134
+ { filesize: 1234, inline: true, key: 'attachments/5/requests/000/000/777/abc/file1.png' },
135
+ { filesize: 9876, inline: true, key: 'attachments/5/requests/000/000/777/abc/file2.jpg' }
136
+ ],
137
+ subject: 'Foobar'
138
+ })
139
+ end
140
+ end
122
141
 
123
- it 'should report an error when 4me confirmation fails' do
124
- stub_request(:post, 'https://itrp.s3.amazonaws.com/').with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: 'OK', status: 303, headers: {'Location' => 'https://mycompany.4me.com/s3_success?sig=99e82e8a046'})
125
- stub_request(:get, "https://api.4me.com/v1/s3_success?sig=99e82e8a046&key=#{@key}").with(basic_auth: ['secret', 'x']).to_return(body: {message: 'oops!'}.to_json)
126
- expect_log('Request failed: oops!', :error)
127
- expect_log("Attachment upload failed: 4me confirmation s3_success?sig=99e82e8a046 for #{@key} failed: oops!", :error)
128
- expect(@attachments.send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", false)).to be_nil
129
- end
142
+ context 'field attachments and rich text inline attachments' do
143
+ it 'should upload, and replace the data in place' do
144
+ a = attachments(authentication, '/requests')
145
+ resp = {
146
+ provider: 'local',
147
+ upload_uri: 'https://widget.example.com/attachments',
148
+ local: {
149
+ key: 'attachments/5/requests/000/000/777/abc/${filename}',
150
+ x_4me_expiration: '2020-11-01T23:59:59Z',
151
+ x_4me_signature: 'foobar'
152
+ }
153
+ }
154
+ stub_request(:get, 'https://api.4me.com/v1/attachments/storage').with(credentials(authentication)).to_return(status: 200, body: resp.to_json)
155
+
156
+ expect(a).to(receive(:upload_attachment).with('/tmp/file3.log').ordered { { key: 'attachments/5/requests/000/000/777/abc/file3.log', filesize: 5678 } })
157
+ expect(a).to(receive(:upload_attachment).with('/tmp/file1.png').ordered { { key: 'attachments/5/requests/000/000/777/abc/file1.png', filesize: 1234 } })
158
+ expect(a).to(receive(:upload_attachment).with('/tmp/file2.jpg').ordered { { key: 'attachments/5/requests/000/000/777/abc/file2.jpg', filesize: 9876 } })
159
+
160
+ data = {
161
+ subject: 'Foobar',
162
+ note: 'Foo [note_attachments: 2] Bar [note_attachments: 1]',
163
+ note_attachments: ['/tmp/file3.log', '/tmp/file1.png', '/tmp/file2.jpg']
164
+ }
165
+ a.upload_attachments!(data)
130
166
 
131
- it 'should raise an exception when AWS upload fails' do
132
- stub_request(:post, 'https://itrp.s3.amazonaws.com/').with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: %(<?xml version="1.0" encoding="UTF-8"?>\n<Error><Code>AccessDenied</Code><Message>Invalid according to Policy</Message><RequestId>1FECC4B719E426B1</RequestId><HostId>15+14lXt+HlF</HostId></Error>), status: 303, headers: {'Location' => 'https://mycompany.4me.com/s3_success?sig=99e82e8a046'})
133
- message = "Attachment upload failed: AWS upload to https://itrp.s3.amazonaws.com/ for #{@key} failed: Invalid according to Policy"
134
- expect{ @attachments.send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", true) }.to raise_error(::Sdk4me::UploadFailed, message)
167
+ expect(data).to eq({
168
+ note: 'Foo ![](attachments/5/requests/000/000/777/abc/file2.jpg) Bar ![](attachments/5/requests/000/000/777/abc/file1.png)',
169
+ note_attachments: [
170
+ { filesize: 5678, key: 'attachments/5/requests/000/000/777/abc/file3.log' },
171
+ { filesize: 1234, inline: true, key: 'attachments/5/requests/000/000/777/abc/file1.png' },
172
+ { filesize: 9876, inline: true, key: 'attachments/5/requests/000/000/777/abc/file2.jpg' }
173
+ ],
174
+ subject: 'Foobar'
175
+ })
176
+ end
177
+
178
+ it 'failed uploads' do
179
+ a = attachments(authentication, '/requests')
180
+ resp = {
181
+ provider: 'local',
182
+ upload_uri: 'https://widget.example.com/attachments',
183
+ local: {
184
+ key: 'attachments/5/requests/000/000/777/abc/${filename}',
185
+ x_4me_expiration: '2020-11-01T23:59:59Z',
186
+ x_4me_signature: 'foobar'
187
+ }
188
+ }
189
+ stub_request(:get, 'https://api.4me.com/v1/attachments/storage').with(credentials(authentication)).to_return(status: 200, body: resp.to_json)
190
+
191
+ expect_log('Attachment upload failed: file does not exist: /tmp/doesnotexist.png', :error)
192
+
193
+ data = {
194
+ subject: 'Foobar',
195
+ note: 'Foo [note_attachments: 2] Bar [note_attachments: 1]',
196
+ note_attachments: ['/tmp/doesnotexist.png']
197
+ }
198
+ expect { a.upload_attachments!(data) }.to raise_error(::Sdk4me::UploadFailed, 'Attachment upload failed: file does not exist: /tmp/doesnotexist.png')
199
+ end
200
+ end
135
201
  end
136
- end
137
202
 
138
- context '4me' do
139
- before(:each) do
140
- @sdk4me_conf = {
203
+ context :upload_attachment do
204
+ before(:each) do
205
+ resp = {
141
206
  provider: 'local',
142
- upload_uri: 'https://api.4me.com/attachments',
143
- upload_path: 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/'
144
- }
145
- @key_template = 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}'
146
- @key = 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/upload.txt'
147
-
148
- @multi_part_body = "--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"Content-Type\"\r\n\r\napplication/octet-stream\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"file\"; filename=\"#{@spec_dir}/support/fixtures/upload.txt\"\r\nContent-Type: text/plain\r\n\r\ncontent\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\nattachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}\r\n--0123456789ABLEWASIEREISAWELBA9876543210--"
149
- @multi_part_headers = {'Accept'=>'*/*', 'Content-Type'=>'multipart/form-data; boundary=0123456789ABLEWASIEREISAWELBA9876543210', 'User-Agent'=>'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6'}
150
- end
207
+ upload_uri: 'https://widget.example.com/attachments',
208
+ local: {
209
+ key: 'attachments/5/requests/000/000/777/abc/${filename}',
210
+ x_4me_expiration: '2020-11-01T23:59:59Z',
211
+ x_4me_signature: 'foobar'
212
+ }
213
+ }
214
+ stub_request(:get, 'https://api.4me.com/v1/attachments/storage').with(credentials(authentication)).to_return(status: 200, body: resp.to_json)
215
+ end
151
216
 
152
- it 'should open a file from disk' do
153
- expect(@attachments).to receive(:upload_to_4me).with(@sdk4me_conf, @key_template, @key, kind_of(File))
154
- expect(@attachments.send(:upload_attachment, @sdk4me_conf, "#{@fixture_dir}/upload.txt", false)).to eq({key: @key, filesize: 7})
217
+ it 'should raise an error when the file could not be found' do
218
+ a = attachments(authentication, '/requests')
219
+ expect(@client).not_to receive(:send_file)
220
+ message = 'Attachment upload failed: file does not exist: /tmp/unknown_file'
221
+ expect_log(message, :error)
222
+ expect { a.send(:upload_attachment, '/tmp/unknown_file') }.to raise_error(::Sdk4me::UploadFailed, message)
223
+ end
155
224
  end
156
225
 
157
- it 'should sent the upload to 4me' do
158
- stub_request(:post, 'https://api.4me.com/v1/attachments').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {}.to_json)
159
- expect(@attachments.send(:upload_attachment, @sdk4me_conf, "#{@fixture_dir}/upload.txt", false)).to eq({key: @key, filesize: 7})
160
- end
226
+ context :s3 do
227
+ before(:each) do
228
+ resp = {
229
+ provider: 's3',
230
+ upload_uri: 'https://example.s3-accelerate.amazonaws.com/',
231
+ s3: {
232
+ acl: 'private',
233
+ key: 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}',
234
+ policy: 'eydlgIH0=',
235
+ success_action_status: 201,
236
+ x_amz_algorithm: 'AWS4-HMAC-SHA256',
237
+ x_amz_credential: 'AKIATRO999Z9E9D2EQ7B/20201107/us-east-1/s3/aws4_request',
238
+ x_amz_date: '20201107T000000Z',
239
+ x_amz_server_side_encryption: 'AES256',
240
+ x_amz_signature: 'nbhdec4k='
241
+ }
242
+ }
243
+ stub_request(:get, 'https://api.4me.com/v1/attachments/storage').with(credentials(authentication)).to_return(status: 200, body: resp.to_json)
244
+ end
245
+
246
+ it 'should upload a file from disk' do
247
+ Tempfile.create('4me_attachments_spec.txt') do |file|
248
+ file << 'foobar'
249
+ file.flush
250
+
251
+ a = attachments(authentication, '/requests')
252
+
253
+ stub_request(:post, 'https://example.s3-accelerate.amazonaws.com/')
254
+ .with(
255
+ body: "--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"Content-Type\"\r\n\r\napplication/octet-stream\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"acl\"\r\n\r\nprivate\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\nattachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"policy\"\r\n\r\neydlgIH0=\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"success_action_status\"\r\n\r\n201\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_amz_algorithm\"\r\n\r\nAWS4-HMAC-SHA256\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_amz_credential\"\r\n\r\nAKIATRO999Z9E9D2EQ7B/20201107/us-east-1/s3/aws4_request\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_amz_date\"\r\n\r\n20201107T000000Z\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_amz_server_side_encryption\"\r\n\r\nAES256\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_amz_signature\"\r\n\r\nnbhdec4k=\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"file\"; filename=\"#{file.path}\"\r\nContent-Type: application/octet-stream\r\n\r\nfoobar\r\n--0123456789ABLEWASIEREISAWELBA9876543210--",
256
+ headers: {
257
+ 'Accept' => '*/*',
258
+ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
259
+ 'Content-Type' => 'multipart/form-data; boundary=0123456789ABLEWASIEREISAWELBA9876543210',
260
+ 'User-Agent' => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6 4me/#{Sdk4me::Client::VERSION}"
261
+ }
262
+ )
263
+ .to_return(status: 200, body: '', headers: {})
264
+
265
+ expect(a.send(:upload_attachment, file.path)).to eq({
266
+ key: "attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/#{File.basename(file.path)}",
267
+ filesize: 6
268
+ })
269
+ end
270
+ end
161
271
 
162
- it 'should report an error when 4me upload fails' do
163
- stub_request(:post, 'https://api.4me.com/v1/attachments').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {message: 'oops!'}.to_json)
164
- expect_log('Request failed: oops!', :error)
165
- expect_log("Attachment upload failed: 4me upload to https://api.4me.com/attachments for #{@key} failed: oops!", :error)
166
- expect(@attachments.send(:upload_attachment, @sdk4me_conf, "#{@fixture_dir}/upload.txt", false)).to be_nil
272
+ it 'should report an error when upload fails' do
273
+ Tempfile.create('4me_attachments_spec.txt') do |file|
274
+ file << 'foobar'
275
+ file.flush
276
+
277
+ a = attachments(authentication, '/requests')
278
+
279
+ stub_request(:post, 'https://example.s3-accelerate.amazonaws.com/')
280
+ .with(
281
+ body: "--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"Content-Type\"\r\n\r\napplication/octet-stream\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"acl\"\r\n\r\nprivate\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\nattachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"policy\"\r\n\r\neydlgIH0=\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"success_action_status\"\r\n\r\n201\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_amz_algorithm\"\r\n\r\nAWS4-HMAC-SHA256\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_amz_credential\"\r\n\r\nAKIATRO999Z9E9D2EQ7B/20201107/us-east-1/s3/aws4_request\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_amz_date\"\r\n\r\n20201107T000000Z\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_amz_server_side_encryption\"\r\n\r\nAES256\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_amz_signature\"\r\n\r\nnbhdec4k=\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"file\"; filename=\"#{file.path}\"\r\nContent-Type: application/octet-stream\r\n\r\nfoobar\r\n--0123456789ABLEWASIEREISAWELBA9876543210--",
282
+ headers: {
283
+ 'Accept' => '*/*',
284
+ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
285
+ 'Content-Type' => 'multipart/form-data; boundary=0123456789ABLEWASIEREISAWELBA9876543210',
286
+ 'User-Agent' => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6 4me/#{Sdk4me::Client::VERSION}"
287
+ }
288
+ )
289
+ .to_return(status: 400, body: '<Error><Message>Foo Bar Failure</Message></Error>', headers: {})
290
+
291
+ key = "attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/#{File.basename(file.path)}"
292
+ message = "Attachment upload failed: AWS S3 upload to https://example.s3-accelerate.amazonaws.com/ for #{key} failed: Foo Bar Failure"
293
+ # expect_log(message, :error)
294
+ expect { a.send(:upload_attachment, file.path) }.to raise_error(::Sdk4me::UploadFailed, message)
295
+ end
296
+ end
167
297
  end
168
298
 
169
- it 'should raise an exception when 4me upload fails' do
170
- stub_request(:post, 'https://api.4me.com/v1/attachments').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {message: 'oops!'}.to_json)
171
- expect_log('Request failed: oops!', :error)
172
- message = "Attachment upload failed: 4me upload to https://api.4me.com/attachments for #{@key} failed: oops!"
173
- expect{ @attachments.send(:upload_attachment, @sdk4me_conf, "#{@fixture_dir}/upload.txt", true) }.to raise_error(::Sdk4me::UploadFailed, message)
299
+ context '4me local' do
300
+ before(:each) do
301
+ resp = {
302
+ provider: 'local',
303
+ upload_uri: 'https://widget.example.com/attachments',
304
+ local: {
305
+ key: 'attachments/5/requests/000/000/777/abc/${filename}',
306
+ x_4me_expiration: '2020-11-01T23:59:59Z',
307
+ x_4me_signature: 'foobar'
308
+ }
309
+ }
310
+ stub_request(:get, 'https://api.4me.com/v1/attachments/storage').with(credentials(authentication)).to_return(status: 200, body: resp.to_json)
311
+ end
312
+
313
+ it 'should upload a file from disk' do
314
+ Tempfile.create('4me_attachments_spec.txt') do |file|
315
+ file << 'foobar'
316
+ file.flush
317
+
318
+ a = attachments(authentication, '/requests')
319
+
320
+ stub_request(:post, 'https://widget.example.com/attachments')
321
+ .with(
322
+ body: "--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"Content-Type\"\r\n\r\napplication/octet-stream\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\nattachments/5/requests/000/000/777/abc/${filename}\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_4me_expiration\"\r\n\r\n2020-11-01T23:59:59Z\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x_4me_signature\"\r\n\r\nfoobar\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"file\"; filename=\"#{file.path}\"\r\nContent-Type: application/octet-stream\r\n\r\nfoobar\r\n--0123456789ABLEWASIEREISAWELBA9876543210--",
323
+ headers: {
324
+ 'Accept' => '*/*',
325
+ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
326
+ 'Authorization' => (authentication == :api_token ? 'Basic c2VjcmV0Ong=' : 'Bearer secret'),
327
+ 'Content-Type' => 'multipart/form-data; boundary=0123456789ABLEWASIEREISAWELBA9876543210',
328
+ 'User-Agent' => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6 4me/#{Sdk4me::Client::VERSION}"
329
+ }
330
+ )
331
+ .to_return(status: 204, body: '', headers: {})
332
+
333
+ expect(a.send(:upload_attachment, file.path)).to eq({
334
+ key: "attachments/5/requests/000/000/777/abc/#{File.basename(file.path)}",
335
+ filesize: 6
336
+ })
337
+ end
338
+ end
174
339
  end
175
340
  end
176
-
177
341
  end
178
342
  end