4me-sdk 1.1.6 → 2.0.0.pre.rc.2
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 +5 -5
- data/4me-sdk.gemspec +12 -13
- data/Gemfile.lock +58 -39
- data/LICENSE +1 -1
- data/README.md +53 -36
- data/lib/sdk4me.rb +7 -5
- data/lib/sdk4me/ca-bundle.crt +1327 -1802
- data/lib/sdk4me/client.rb +110 -86
- data/lib/sdk4me/client/attachments.rb +109 -75
- data/lib/sdk4me/client/multipart.rb +16 -18
- data/lib/sdk4me/client/response.rb +17 -19
- data/lib/sdk4me/client/version.rb +1 -1
- data/spec/lib/sdk4me/attachments_spec.rb +307 -143
- data/spec/lib/sdk4me/certificate_spec.rb +17 -4
- data/spec/lib/sdk4me/client_spec.rb +490 -475
- data/spec/lib/sdk4me/response_spec.rb +249 -232
- data/spec/lib/sdk4me_spec.rb +7 -7
- data/spec/spec_helper.rb +5 -8
- data/spec/support/matchers/never_raise.rb +16 -21
- data/spec/support/util.rb +2 -2
- metadata +38 -24
@@ -8,7 +8,7 @@ require 'mime/types'
|
|
8
8
|
# Created:: 22 Feb 2008
|
9
9
|
module Sdk4me
|
10
10
|
module Multipart
|
11
|
-
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=#{
|
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
|
37
|
-
|
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(
|
48
|
-
@k =
|
49
|
-
@v =
|
45
|
+
def initialize(key, value)
|
46
|
+
@k = key
|
47
|
+
@v = value
|
50
48
|
end
|
51
49
|
|
52
50
|
def to_multipart
|
53
|
-
|
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(
|
63
|
-
@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[
|
71
|
-
|
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
|
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,7 +52,7 @@ module Sdk4me
|
|
57
52
|
def valid?
|
58
53
|
message.nil?
|
59
54
|
end
|
60
|
-
|
55
|
+
alias success? valid?
|
61
56
|
|
62
57
|
# +true+ in case of a HTTP 5xx error
|
63
58
|
def failure?
|
@@ -69,15 +64,19 @@ module Sdk4me
|
|
69
64
|
# @param keys: a single key or a key-path separated by comma
|
70
65
|
def[](*keys)
|
71
66
|
values = json.is_a?(Array) ? json : [json]
|
72
|
-
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 } }
|
73
68
|
json.is_a?(Array) ? values : values.first
|
74
69
|
end
|
75
70
|
|
76
71
|
# The nr of resources found
|
77
72
|
def size
|
78
|
-
@size ||= message
|
73
|
+
@size ||= if message
|
74
|
+
0
|
75
|
+
else
|
76
|
+
json.is_a?(Array) ? json.size : 1
|
77
|
+
end
|
79
78
|
end
|
80
|
-
alias
|
79
|
+
alias count size
|
81
80
|
|
82
81
|
# pagination - per page
|
83
82
|
def per_page
|
@@ -103,12 +102,12 @@ module Sdk4me
|
|
103
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.
|
104
103
|
def pagination_link(relation)
|
105
104
|
# split on ',' select the [url] in '<[url]>; rel="[relation]"', compact to all url's found (at most one) and take the first
|
106
|
-
(@pagination_links ||= {})[relation] ||= @response.header['Link'] && @response.header['Link'].split(/,\s*<?/).map{ |link| link[/^\s*<?(.*?)>?;\s*rel="#{relation
|
105
|
+
(@pagination_links ||= {})[relation] ||= @response.header['Link'] && @response.header['Link'].split(/,\s*<?/).map { |link| link[/^\s*<?(.*?)>?;\s*rel="#{relation}"\s*$/, 1] }.compact.first
|
107
106
|
end
|
108
107
|
|
109
108
|
# pagination urls (relative paths without server) - relations :first, :prev, :next, :last
|
110
109
|
def pagination_relative_link(relation)
|
111
|
-
(@pagination_relative_links ||= {})[relation] ||= pagination_link(relation) && pagination_link(relation)[
|
110
|
+
(@pagination_relative_links ||= {})[relation] ||= pagination_link(relation) && pagination_link(relation)[%r{^https?://[^/]*(.*)}, 1]
|
112
111
|
end
|
113
112
|
|
114
113
|
# +true+ if the response is invalid because of throttling
|
@@ -117,12 +116,11 @@ module Sdk4me
|
|
117
116
|
end
|
118
117
|
|
119
118
|
def retry_after
|
120
|
-
@
|
119
|
+
@retry_after ||= @response.header['Retry-After'].to_i
|
121
120
|
end
|
122
121
|
|
123
122
|
def to_s
|
124
123
|
valid? ? json.to_s : message
|
125
124
|
end
|
126
|
-
|
127
125
|
end
|
128
126
|
end
|
@@ -1,178 +1,342 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'tempfile'
|
2
3
|
|
3
4
|
describe Sdk4me::Attachments do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
107
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
131
|
+
expect(data).to eq({
|
132
|
+
note: 'Foo  Bar ',
|
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
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
167
|
+
expect(data).to eq({
|
168
|
+
note: 'Foo  Bar ',
|
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
|
-
|
139
|
-
|
140
|
-
|
203
|
+
context :upload_attachment do
|
204
|
+
before(:each) do
|
205
|
+
resp = {
|
141
206
|
provider: 'local',
|
142
|
-
upload_uri: 'https://
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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, headers: {}, body: %(<?xml version="1.0" encoding="UTF-8"?>\n<PostResponse><Location>foo</Location><Bucket>example</Bucket><Key>attachments/5/zxxb4ot60xfd6sjg/s3test.txt</Key><ETag>"bar"</ETag></PostResponse>))
|
264
|
+
|
265
|
+
expect(a.send(:upload_attachment, file.path)).to eq({
|
266
|
+
key: 'attachments/5/zxxb4ot60xfd6sjg/s3test.txt',
|
267
|
+
filesize: 6
|
268
|
+
})
|
269
|
+
end
|
270
|
+
end
|
161
271
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|