4me-sdk 1.2.0 → 2.0.0.pre.rc.1
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/4me-sdk.gemspec +12 -13
- data/Gemfile.lock +51 -30
- data/LICENSE +1 -1
- data/README.md +31 -23
- data/lib/sdk4me.rb +6 -5
- data/lib/sdk4me/ca-bundle.crt +1327 -1802
- data/lib/sdk4me/client.rb +96 -84
- data/lib/sdk4me/client/attachments.rb +103 -116
- 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 +282 -203
- data/spec/lib/sdk4me/certificate_spec.rb +3 -4
- data/spec/lib/sdk4me/client_spec.rb +165 -157
- data/spec/lib/sdk4me/response_spec.rb +25 -29
- 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 +35 -20
@@ -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,262 +1,341 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'tempfile'
|
2
3
|
|
3
4
|
describe Sdk4me::Attachments do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
12
|
-
Sdk4me::Attachments.new(client)
|
13
|
-
end
|
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)
|
14
12
|
end
|
15
13
|
|
16
14
|
def credentials(authentication)
|
17
15
|
if authentication == :api_token
|
18
|
-
{ basic_auth: [
|
16
|
+
{ basic_auth: %w[secret x] }
|
19
17
|
else
|
20
|
-
{ headers: {'Authorization' => 'Bearer secret'} }
|
18
|
+
{ headers: { 'Authorization' => 'Bearer secret' } }
|
21
19
|
end
|
22
20
|
end
|
23
21
|
|
24
|
-
[
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
expect(
|
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 })
|
31
30
|
end
|
32
31
|
|
33
|
-
it 'should not do anything when
|
34
|
-
|
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 })
|
35
36
|
end
|
36
37
|
|
37
|
-
it 'should not do anything when
|
38
|
-
|
39
|
-
expect(
|
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] })
|
40
43
|
end
|
41
44
|
|
42
|
-
it 'should
|
43
|
-
|
44
|
-
|
45
|
-
|
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')
|
46
52
|
end
|
47
53
|
|
48
|
-
it 'should
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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)
|
59
66
|
|
60
|
-
|
61
|
-
|
62
|
-
[:contracts, :remarks],
|
63
|
-
[:cis, :remarks],
|
64
|
-
[:flsas, :remarks],
|
65
|
-
[:slas, :remarks],
|
66
|
-
[:service_instances, :remarks],
|
67
|
-
[:service_offerings, :summary],
|
68
|
-
[:any_other_model, :note]].each do |model, attribute|
|
69
|
-
|
70
|
-
it "should replace :attachments with :#{attribute}_attachments after upload at /#{model}" do
|
71
|
-
stub_request(:get, "https://api.4me.com/v1/#{model}/new?attachment_upload_token=true").with(credentials(authentication)).to_return(body: {storage_upload: 'conf'}.to_json)
|
72
|
-
expect(attachments(authentication)).to receive(:upload_attachment).with('conf', 'file1.png', false).ordered{ 'uploaded file1.png' }
|
73
|
-
expect(attachments(authentication)).to receive(:upload_attachment).with('conf', 'file2.zip', false).ordered{ 'uploaded file2.zip' }
|
74
|
-
data = {leave: 'me alone', attachments: %w(file1.png file2.zip)}
|
75
|
-
attachments(authentication).upload_attachments!("/#{model}", data)
|
76
|
-
expect(data[:attachments]).to be_nil
|
77
|
-
expect(data[:leave]).to eq('me alone')
|
78
|
-
expect(data[:"#{attribute}_attachments"]).to eq(['uploaded file1.png', 'uploaded file2.zip'].to_json)
|
79
|
-
end
|
80
|
-
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 } })
|
81
69
|
|
82
|
-
|
83
|
-
|
84
|
-
expect(
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
expect(data[:attachments_exception]).to be_nil
|
89
|
-
expect(data[:leave]).to eq('me alone')
|
90
|
-
expect(data[:note_attachments]).to eq(['uploaded file1.png'].to_json)
|
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
|
+
] })
|
91
76
|
end
|
92
77
|
end
|
93
78
|
|
94
|
-
context 'inline' do
|
95
|
-
it 'should not do anything when no [
|
96
|
-
|
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]' })
|
97
86
|
end
|
98
87
|
|
99
|
-
it 'should not do anything when
|
100
|
-
|
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]' })
|
101
94
|
end
|
102
95
|
|
103
|
-
it 'should
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
expect{
|
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')
|
113
106
|
end
|
114
107
|
|
115
|
-
it 'should
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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)
|
120
120
|
|
121
|
-
|
122
|
-
|
123
|
-
[:contracts, :remarks],
|
124
|
-
[:cis, :remarks],
|
125
|
-
[:flsas, :remarks],
|
126
|
-
[:slas, :remarks],
|
127
|
-
[:service_instances, :remarks],
|
128
|
-
[:service_offerings, :summary],
|
129
|
-
[:any_other_model, :note]].each do |model, attribute|
|
130
|
-
|
131
|
-
it "should replace :attachments with :#{attribute}_attachments after upload at /#{model}" do
|
132
|
-
stub_request(:get, "https://api.4me.com/v1/#{model}/new?attachment_upload_token=true").with(credentials(authentication)).to_return(body: {storage_upload: 'conf'}.to_json)
|
133
|
-
expect(attachments(authentication)).to receive(:upload_attachment).with('conf', 'file1.png', false).ordered{ {key: 'uploaded file1.png'} }
|
134
|
-
expect(attachments(authentication)).to receive(:upload_attachment).with('conf', 'file2.zip', false).ordered{ {key: 'uploaded file2.zip'} }
|
135
|
-
data = {leave: 'me alone', attribute => '[attachment:file1.png] and [attachment:file2.zip]'}
|
136
|
-
attachments(authentication).upload_attachments!("/#{model}", data)
|
137
|
-
expect(data[:attachments]).to be_nil
|
138
|
-
expect(data[:leave]).to eq('me alone')
|
139
|
-
expect(data[:"#{attribute}_attachments"]).to eq([{key: 'uploaded file1.png', inline: true}, {key: 'uploaded file2.zip', inline: true}].to_json)
|
140
|
-
expect(data[:"#{attribute}"]).to eq(' and ')
|
141
|
-
end
|
142
|
-
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 } })
|
143
123
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
expect(data
|
152
|
-
|
153
|
-
|
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)
|
130
|
+
|
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
|
+
})
|
154
139
|
end
|
155
140
|
end
|
156
141
|
|
157
|
-
|
158
|
-
|
159
|
-
|
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)
|
160
155
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
end
|
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 } })
|
165
159
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
context 'aws' do
|
172
|
-
before(:each) do
|
173
|
-
@aws_conf = {
|
174
|
-
provider: 'aws',
|
175
|
-
upload_uri: 'https://itrp.s3.amazonaws.com/',
|
176
|
-
access_key: 'AKIA6RYQ',
|
177
|
-
success_url: 'https://mycompany.4me.com/s3_success?sig=99e82e8a046',
|
178
|
-
policy: 'eydlgIH0=',
|
179
|
-
signature: 'nbhdec4k=',
|
180
|
-
upload_path: 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/'
|
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']
|
181
164
|
}
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
165
|
+
a.upload_attachments!(data)
|
166
|
+
|
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
|
+
})
|
187
176
|
end
|
188
177
|
|
189
|
-
it '
|
190
|
-
|
191
|
-
|
192
|
-
|
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)
|
193
190
|
|
194
|
-
|
195
|
-
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'})
|
196
|
-
stub_request(:get, "https://api.4me.com/v1/s3_success?sig=99e82e8a046&key=#{@key}").with(credentials(authentication)).to_return(body: {}.to_json)
|
197
|
-
expect(attachments(authentication).send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", false)).to eq({key: @key, filesize: 7})
|
198
|
-
end
|
191
|
+
expect_log('Attachment upload failed: file does not exist: /tmp/doesnotexist.png', :error)
|
199
192
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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')
|
204
199
|
end
|
200
|
+
end
|
201
|
+
end
|
205
202
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
203
|
+
context :upload_attachment do
|
204
|
+
before(:each) do
|
205
|
+
resp = {
|
206
|
+
provider: 'local',
|
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
|
213
216
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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)
|
219
223
|
end
|
224
|
+
end
|
220
225
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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='
|
227
241
|
}
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
@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--"
|
232
|
-
@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'}
|
233
|
-
end
|
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
|
234
245
|
|
235
|
-
|
236
|
-
|
237
|
-
|
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
|
+
})
|
238
269
|
end
|
270
|
+
end
|
239
271
|
|
240
|
-
|
241
|
-
|
242
|
-
|
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)
|
243
295
|
end
|
296
|
+
end
|
297
|
+
end
|
244
298
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
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
|
251
312
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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
|
+
})
|
257
337
|
end
|
258
338
|
end
|
259
|
-
|
260
339
|
end
|
261
340
|
end
|
262
341
|
end
|