transloadit 3.1.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitattributes +44 -5
- data/.github/workflows/ci.yml +18 -15
- data/.gitignore +1 -1
- data/.standard.yml +2 -0
- data/CHANGELOG.md +17 -0
- data/Dockerfile +1 -1
- data/Gemfile +4 -0
- data/MIGRATING_TO_V4.md +136 -0
- data/README.md +55 -3
- data/lib/transloadit/assembly.rb +73 -14
- data/lib/transloadit/exception.rb +14 -6
- data/lib/transloadit/request.rb +54 -22
- data/lib/transloadit/response.rb +36 -34
- data/lib/transloadit/version.rb +1 -1
- data/lib/transloadit.rb +5 -1
- data/test/test_helper.rb +26 -0
- data/test/unit/test_transloadit.rb +1 -1
- data/test/unit/transloadit/test_assembly.rb +92 -9
- data/test/unit/transloadit/test_assembly_uploads.rb +272 -0
- data/test/unit/transloadit/test_request.rb +1 -19
- data/test/unit/transloadit/test_response.rb +33 -31
- data/test/unit/transloadit/test_transport_compatibility.rb +222 -0
- data/transloadit.gemspec +5 -4
- metadata +34 -14
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "stringio"
|
|
3
|
+
require "tempfile"
|
|
4
|
+
|
|
5
|
+
describe "Assembly uploads and custom fields" do
|
|
6
|
+
include WebMock::API
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
WebMock.reset!
|
|
10
|
+
@transloadit = Transloadit.new(key: "key", secret: "secret")
|
|
11
|
+
@requests = []
|
|
12
|
+
stub_request(:post, "https://api2.transloadit.com/assemblies").to_return do |request|
|
|
13
|
+
@requests << request
|
|
14
|
+
{body: '{"ok":"ASSEMBLY_COMPLETED"}'}
|
|
15
|
+
end
|
|
16
|
+
@fields = {
|
|
17
|
+
"count" => 2,
|
|
18
|
+
"enabled" => false,
|
|
19
|
+
"ratio" => 1.5,
|
|
20
|
+
"empty" => nil,
|
|
21
|
+
"tags" => ["hello & goodbye", 3, true, nil],
|
|
22
|
+
"details" => {"caption" => "café + 猫", "visible" => true}
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
after do
|
|
27
|
+
WebMock.reset!
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "sends typed custom fields only in signed JSON without uploads" do
|
|
31
|
+
@transloadit.assembly(fields: @fields).create!
|
|
32
|
+
|
|
33
|
+
_(body_parts.map(&:first)).must_equal %w[params signature]
|
|
34
|
+
_(signed_params.fetch("fields")).must_equal @fields
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "sends named File parts separately from typed fields with the same name" do
|
|
38
|
+
Tempfile.create(["assembly-upload", ".bin"]) do |file|
|
|
39
|
+
file.binmode
|
|
40
|
+
file.write("named\x00\xff upload".b)
|
|
41
|
+
file.flush
|
|
42
|
+
fields = @fields.merge("hero" => "metadata for the hero")
|
|
43
|
+
uploads = {hero: file}.freeze
|
|
44
|
+
assembly = @transloadit.assembly(uploads: uploads, fields: fields.freeze)
|
|
45
|
+
|
|
46
|
+
_, warning = capture_io { assembly.create! }
|
|
47
|
+
|
|
48
|
+
_(warning).wont_include "Passing upload IOs in fields"
|
|
49
|
+
_(body_parts.map(&:first)).must_equal %w[params signature hero]
|
|
50
|
+
_(body_parts.to_h.fetch("hero")).must_equal "named\x00\xff upload".b
|
|
51
|
+
_(signed_params.fetch("fields")).must_equal fields
|
|
52
|
+
_(signed_params).wont_include "uploads"
|
|
53
|
+
_(assembly.to_hash).wont_include :uploads
|
|
54
|
+
_(file.closed?).must_equal false
|
|
55
|
+
_(uploads[:hero]).must_equal file
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "allows per-call uploads and fields to override constructor defaults" do
|
|
60
|
+
defaults = {unused: StringIO.new("unused upload")}
|
|
61
|
+
assembly = @transloadit.assembly(uploads: defaults, fields: {unused: true})
|
|
62
|
+
upload = StringIO.new("named upload")
|
|
63
|
+
positional = StringIO.new("positional upload")
|
|
64
|
+
|
|
65
|
+
assembly.create!(positional, uploads: {"hero" => upload}, fields: @fields)
|
|
66
|
+
|
|
67
|
+
_(body_parts.map(&:first)).must_equal %w[params signature hero file_0]
|
|
68
|
+
_(body_parts.to_h.fetch("hero")).must_equal "named upload"
|
|
69
|
+
_(body_parts.to_h.fetch("file_0")).must_equal "positional upload"
|
|
70
|
+
_(signed_params.fetch("fields")).must_equal @fields
|
|
71
|
+
_(signed_params).wont_include "uploads"
|
|
72
|
+
_(upload.closed?).must_equal false
|
|
73
|
+
_(positional.closed?).must_equal false
|
|
74
|
+
_(defaults.keys).must_equal [:unused]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "preserves legacy mixed fields while deprecating only their upload entries" do
|
|
78
|
+
upload = StringIO.new("legacy upload")
|
|
79
|
+
fields = @fields.merge("hero" => upload).freeze
|
|
80
|
+
assembly = @transloadit.assembly(fields: fields)
|
|
81
|
+
|
|
82
|
+
_, warning = capture_io { assembly.create! }
|
|
83
|
+
|
|
84
|
+
_(warning).must_match(/fields.*deprecated.*uploads/)
|
|
85
|
+
_(body_parts.map(&:first)).must_equal %w[params signature hero]
|
|
86
|
+
_(body_parts.to_h.fetch("hero")).must_equal "legacy upload"
|
|
87
|
+
_(signed_params.fetch("fields")).must_equal @fields
|
|
88
|
+
_(assembly.to_hash[:fields]).must_equal @fields
|
|
89
|
+
_(JSON.parse(assembly.to_json).fetch("fields")).must_equal @fields
|
|
90
|
+
_(fields.fetch("hero")).must_equal upload
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "preserves content's create! fields override for legacy named Files" do
|
|
94
|
+
File.open("lib/transloadit/version.rb", "rb") do |file|
|
|
95
|
+
assembly = @transloadit.assembly(fields: {file: file})
|
|
96
|
+
|
|
97
|
+
_, warning = capture_io { assembly.create!(fields: @fields) }
|
|
98
|
+
|
|
99
|
+
_(warning).must_include "Passing upload IOs in fields is deprecated"
|
|
100
|
+
_(body_parts.map(&:first)).must_equal %w[params signature file]
|
|
101
|
+
_(body_parts.to_h.fetch("file")).must_equal File.binread(file.path)
|
|
102
|
+
_(signed_params.fetch("fields")).must_equal @fields
|
|
103
|
+
_(file.closed?).must_equal false
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "separates deprecated uploads in per-call fields from custom fields" do
|
|
108
|
+
fields = @fields.merge("hero" => StringIO.new("per-call legacy upload"))
|
|
109
|
+
|
|
110
|
+
_, warning = capture_io { @transloadit.assembly.create!(fields: fields) }
|
|
111
|
+
|
|
112
|
+
_(warning).must_include "Passing upload IOs in fields is deprecated"
|
|
113
|
+
_(body_parts.map(&:first)).must_equal %w[params signature hero]
|
|
114
|
+
_(body_parts.to_h.fetch("hero")).must_equal "per-call legacy upload"
|
|
115
|
+
_(signed_params.fetch("fields")).must_equal @fields
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "keeps positional file_N names and priority over legacy upload names" do
|
|
119
|
+
assembly = @transloadit.assembly(fields: {"file_0" => StringIO.new("legacy")})
|
|
120
|
+
|
|
121
|
+
capture_io { assembly.create!(StringIO.new("first"), StringIO.new("second")) }
|
|
122
|
+
|
|
123
|
+
_(body_parts.map(&:first)).must_equal %w[params signature file_0 file_1]
|
|
124
|
+
_(body_parts.to_h.fetch("file_0")).must_equal "first"
|
|
125
|
+
_(body_parts.to_h.fetch("file_1")).must_equal "second"
|
|
126
|
+
_(signed_params.fetch("fields")).must_equal({})
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "lets explicit uploads replace deprecated entries with the same wire name" do
|
|
130
|
+
assembly = @transloadit.assembly(fields: {hero: StringIO.new("legacy")})
|
|
131
|
+
|
|
132
|
+
capture_io { assembly.create!(uploads: {"hero" => StringIO.new("explicit")}) }
|
|
133
|
+
|
|
134
|
+
_(body_parts.map(&:first)).must_equal %w[params signature hero]
|
|
135
|
+
_(body_parts.to_h.fetch("hero")).must_equal "explicit"
|
|
136
|
+
_(signed_params.fetch("fields")).must_equal({})
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "supports typed per-call fields and an explicitly empty upload override" do
|
|
140
|
+
assembly = @transloadit.assembly(uploads: {hero: StringIO.new("unused")})
|
|
141
|
+
|
|
142
|
+
assembly.create!(uploads: {}, fields: @fields)
|
|
143
|
+
|
|
144
|
+
_(body_parts.map(&:first)).must_equal %w[params signature]
|
|
145
|
+
_(signed_params.fetch("fields")).must_equal @fields
|
|
146
|
+
_(signed_params).wont_include "uploads"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "preserves deprecated fields uploads when only the uploads option is cleared" do
|
|
150
|
+
assembly = @transloadit.assembly(
|
|
151
|
+
uploads: {unused: StringIO.new("unused")},
|
|
152
|
+
fields: {hero: StringIO.new("legacy upload")}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
capture_io { assembly.create!(uploads: {}) }
|
|
156
|
+
|
|
157
|
+
_(body_parts.map(&:first)).must_equal %w[params signature hero]
|
|
158
|
+
_(body_parts.to_h.fetch("hero")).must_equal "legacy upload"
|
|
159
|
+
_(signed_params.fetch("fields")).must_equal({})
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "rejects non-IO named uploads before making a request" do
|
|
163
|
+
error = assert_raises ArgumentError do
|
|
164
|
+
@transloadit.assembly(uploads: {hero: "path/to/file.jpg"}).create!
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
_(error.message).must_include "IO"
|
|
168
|
+
_(@requests).must_be_empty
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "rejects upload names reserved for params and signature" do
|
|
172
|
+
[:params, "signature"].each do |name|
|
|
173
|
+
error = assert_raises ArgumentError do
|
|
174
|
+
@transloadit.assembly.create!(uploads: {name => StringIO.new("upload")})
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
_(error.message).must_include "reserved"
|
|
178
|
+
end
|
|
179
|
+
_(@requests).must_be_empty
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "keeps reserved names safe when they are custom fields" do
|
|
183
|
+
fields = {"params" => {"count" => 2}, "signature" => false}
|
|
184
|
+
|
|
185
|
+
@transloadit.assembly(fields: fields).create!
|
|
186
|
+
|
|
187
|
+
_(body_parts.map(&:first)).must_equal %w[params signature]
|
|
188
|
+
_(signed_params.fetch("fields")).must_equal fields
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "retries named uploads with the same bytes and signed custom fields" do
|
|
192
|
+
rate_limit_first_request
|
|
193
|
+
upload = StringIO.new("retry upload")
|
|
194
|
+
|
|
195
|
+
capture_io do
|
|
196
|
+
response = @transloadit.assembly(uploads: {hero: upload}, fields: @fields).create!
|
|
197
|
+
_(response.completed?).must_equal true
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
_(@requests.length).must_equal 2
|
|
201
|
+
@requests.each do |request|
|
|
202
|
+
_(body_parts(request).map(&:first)).must_equal %w[params signature hero]
|
|
203
|
+
_(body_parts(request).to_h.fetch("hero")).must_equal "retry upload"
|
|
204
|
+
_(signed_params(request).fetch("fields")).must_equal @fields
|
|
205
|
+
end
|
|
206
|
+
_(upload.closed?).must_equal false
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it "retries in-memory uploads from their original position" do
|
|
210
|
+
rate_limit_first_request
|
|
211
|
+
upload = StringIO.new("header:retry upload")
|
|
212
|
+
upload.pos = "header:".length
|
|
213
|
+
|
|
214
|
+
capture_io { @transloadit.assembly(uploads: {hero: upload}, fields: @fields).create! }
|
|
215
|
+
|
|
216
|
+
_(@requests.length).must_equal 2
|
|
217
|
+
@requests.each do |request|
|
|
218
|
+
_(body_parts(request).to_h.fetch("hero")).must_equal "retry upload"
|
|
219
|
+
_(signed_params(request).fetch("fields")).must_equal @fields
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "fails clearly instead of retrying a consumed non-seekable upload" do
|
|
224
|
+
rate_limit_first_request
|
|
225
|
+
upload = StringIO.new("streamed upload")
|
|
226
|
+
[:pos, :seek, :rewind].each do |method|
|
|
227
|
+
upload.define_singleton_method(method) { |*| raise Errno::ESPIPE }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
error = assert_raises IOError do
|
|
231
|
+
capture_io { @transloadit.assembly(uploads: {hero: upload}).create! }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
_(error.message).must_include "Cannot retry"
|
|
235
|
+
_(@requests.length).must_equal 1
|
|
236
|
+
_(body_parts.to_h.fetch("hero")).must_equal "streamed upload"
|
|
237
|
+
_(upload.closed?).must_equal false
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def rate_limit_first_request
|
|
241
|
+
stub_request(:post, "https://api2.transloadit.com/assemblies").to_return do |request|
|
|
242
|
+
@requests << request
|
|
243
|
+
body = if @requests.length == 1
|
|
244
|
+
'{"error":"RATE_LIMIT_REACHED","info":{"retryIn":0}}'
|
|
245
|
+
else
|
|
246
|
+
'{"ok":"ASSEMBLY_COMPLETED"}'
|
|
247
|
+
end
|
|
248
|
+
{body: body}
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def body_parts(request = @requests.last)
|
|
253
|
+
content_type = request.headers.fetch("Content-Type")
|
|
254
|
+
return URI.decode_www_form(request.body) unless content_type.start_with?("multipart/form-data")
|
|
255
|
+
|
|
256
|
+
boundary = content_type.split("boundary=", 2).last
|
|
257
|
+
request.body.b.split("--#{boundary}".b).filter_map do |part|
|
|
258
|
+
headers, value = part.split("\r\n\r\n", 2)
|
|
259
|
+
next unless value
|
|
260
|
+
|
|
261
|
+
[headers[/name="([^"]+)"/, 1], value.delete_suffix("\r\n")]
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def signed_params(request = @requests.last)
|
|
266
|
+
parts = body_parts(request).to_h
|
|
267
|
+
json = parts.fetch("params")
|
|
268
|
+
signature = OpenSSL::HMAC.hexdigest("sha384", "secret", json)
|
|
269
|
+
_(parts.fetch("signature")).must_equal "sha384:#{signature}"
|
|
270
|
+
JSON.parse(json)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
@@ -83,25 +83,6 @@ describe Transloadit::Request do
|
|
|
83
83
|
lib_path = File.expand_path("../../../lib", __dir__)
|
|
84
84
|
|
|
85
85
|
Dir.mktmpdir do |stub_dir|
|
|
86
|
-
File.write(File.join(stub_dir, "rest-client.rb"), <<~RUBY)
|
|
87
|
-
module RestClient
|
|
88
|
-
class Response; end
|
|
89
|
-
|
|
90
|
-
class Resource
|
|
91
|
-
def initialize(*); end
|
|
92
|
-
def [](*); self; end
|
|
93
|
-
def get(*); Response.new; end
|
|
94
|
-
def post(*); Response.new; end
|
|
95
|
-
def put(*); Response.new; end
|
|
96
|
-
def delete(*); Response.new; end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
module Exceptions
|
|
100
|
-
class OpenTimeout < StandardError; end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
RUBY
|
|
104
|
-
|
|
105
86
|
File.write(File.join(stub_dir, "multi_json.rb"), <<~RUBY)
|
|
106
87
|
require "json"
|
|
107
88
|
|
|
@@ -123,6 +104,7 @@ describe Transloadit::Request do
|
|
|
123
104
|
begin
|
|
124
105
|
require "transloadit/request"
|
|
125
106
|
Transloadit::Request.new("/")
|
|
107
|
+
raise "RestClient was loaded" if defined?(RestClient)
|
|
126
108
|
rescue StandardError => e
|
|
127
109
|
warn e.full_message
|
|
128
110
|
exit 1
|
|
@@ -3,17 +3,36 @@ require "test_helper"
|
|
|
3
3
|
describe Transloadit::Response do
|
|
4
4
|
request_uri = "https://api2.jane.transloadit.com/assemblies/76fe5df1c93a0a530f3e583805cf98b4"
|
|
5
5
|
|
|
6
|
-
it "must allow
|
|
7
|
-
response = Transloadit::Response.new("
|
|
6
|
+
it "must allow initialization" do
|
|
7
|
+
response = Transloadit::Response.new(body: "{}", headers: {}, status: 200)
|
|
8
8
|
_(response.class).must_equal Transloadit::Response
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
it "must replace body, headers, and status together" do
|
|
12
|
+
response = Transloadit::Response.new(
|
|
13
|
+
body: '{"ok":"ASSEMBLY_EXECUTING"}',
|
|
14
|
+
headers: {"X-Request-Id" => "old-request"},
|
|
15
|
+
status: 202
|
|
16
|
+
)
|
|
17
|
+
replacement = Transloadit::Response.new(
|
|
18
|
+
body: '{"ok":"ASSEMBLY_COMPLETED"}',
|
|
19
|
+
headers: {"X-Request-Id" => "new-request"},
|
|
20
|
+
status: 200
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
returned = response.replace(replacement)
|
|
24
|
+
|
|
25
|
+
_(returned).must_be_same_as response
|
|
26
|
+
_(response["ok"]).must_equal "ASSEMBLY_COMPLETED"
|
|
27
|
+
_(response.headers).must_equal x_request_id: "new-request"
|
|
28
|
+
_(response.code).must_equal 200
|
|
29
|
+
_(response.status).must_equal 200
|
|
30
|
+
end
|
|
31
|
+
|
|
11
32
|
describe "when initialized" do
|
|
12
33
|
before do
|
|
13
34
|
VCR.use_cassette "fetch_assembly_ok" do
|
|
14
|
-
@response = Transloadit::
|
|
15
|
-
RestClient::Resource.new(request_uri).get
|
|
16
|
-
)
|
|
35
|
+
@response = Transloadit::Request.new(request_uri).get
|
|
17
36
|
end
|
|
18
37
|
end
|
|
19
38
|
|
|
@@ -41,9 +60,7 @@ describe Transloadit::Response do
|
|
|
41
60
|
describe "when extended as an assembly" do
|
|
42
61
|
before do
|
|
43
62
|
VCR.use_cassette "fetch_assembly_ok" do
|
|
44
|
-
@response = Transloadit::
|
|
45
|
-
RestClient::Resource.new(request_uri).get
|
|
46
|
-
).extend!(Transloadit::Response::Assembly)
|
|
63
|
+
@response = Transloadit::Request.new(request_uri).get.extend!(Transloadit::Response::Assembly)
|
|
47
64
|
end
|
|
48
65
|
end
|
|
49
66
|
|
|
@@ -56,9 +73,6 @@ describe Transloadit::Response do
|
|
|
56
73
|
# TODO: can this be tested better?
|
|
57
74
|
it "must allow reloading the assembly" do
|
|
58
75
|
VCR.use_cassette "fetch_assembly_ok", allow_playback_repeats: true do
|
|
59
|
-
_(@response.send(:__getobj__))
|
|
60
|
-
.wont_be_same_as @response.reload!.send(:__getobj__)
|
|
61
|
-
|
|
62
76
|
_(@response.object_id)
|
|
63
77
|
.must_equal @response.reload!.object_id
|
|
64
78
|
end
|
|
@@ -79,9 +93,7 @@ describe Transloadit::Response do
|
|
|
79
93
|
describe "long-running assembly" do
|
|
80
94
|
before do
|
|
81
95
|
VCR.use_cassette "fetch_assembly_executing" do
|
|
82
|
-
@response = Transloadit::
|
|
83
|
-
RestClient::Resource.new(request_uri).get
|
|
84
|
-
).extend!(Transloadit::Response::Assembly)
|
|
96
|
+
@response = Transloadit::Request.new(request_uri).get.extend!(Transloadit::Response::Assembly)
|
|
85
97
|
end
|
|
86
98
|
end
|
|
87
99
|
|
|
@@ -109,9 +121,7 @@ describe Transloadit::Response do
|
|
|
109
121
|
describe "statuses" do
|
|
110
122
|
it "must allow checking for upload" do
|
|
111
123
|
VCR.use_cassette "fetch_assembly_uploading" do
|
|
112
|
-
@response = Transloadit::
|
|
113
|
-
RestClient::Resource.new(request_uri).get
|
|
114
|
-
).extend!(Transloadit::Response::Assembly)
|
|
124
|
+
@response = Transloadit::Request.new(request_uri).get.extend!(Transloadit::Response::Assembly)
|
|
115
125
|
end
|
|
116
126
|
|
|
117
127
|
_(@response.finished?).must_equal false
|
|
@@ -121,9 +131,7 @@ describe Transloadit::Response do
|
|
|
121
131
|
|
|
122
132
|
it "must allow to check for executing" do
|
|
123
133
|
VCR.use_cassette "fetch_assembly_executing" do
|
|
124
|
-
@response = Transloadit::
|
|
125
|
-
RestClient::Resource.new(request_uri).get
|
|
126
|
-
).extend!(Transloadit::Response::Assembly)
|
|
134
|
+
@response = Transloadit::Request.new(request_uri).get.extend!(Transloadit::Response::Assembly)
|
|
127
135
|
end
|
|
128
136
|
|
|
129
137
|
_(@response.finished?).must_equal false
|
|
@@ -133,11 +141,9 @@ describe Transloadit::Response do
|
|
|
133
141
|
|
|
134
142
|
it "must allow to check for replaying" do
|
|
135
143
|
VCR.use_cassette "replay_assembly" do
|
|
136
|
-
@response = Transloadit::
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
).post({})
|
|
140
|
-
).extend!(Transloadit::Response::Assembly)
|
|
144
|
+
@response = Transloadit::Request.new(
|
|
145
|
+
"https://api2.transloadit.com/assemblies/55c965a063a311e6ba2d379ef10b28f7/replay"
|
|
146
|
+
).post.extend!(Transloadit::Response::Assembly)
|
|
141
147
|
end
|
|
142
148
|
|
|
143
149
|
_(@response.finished?).must_equal false
|
|
@@ -147,9 +153,7 @@ describe Transloadit::Response do
|
|
|
147
153
|
|
|
148
154
|
it "must allow to check for aborted" do
|
|
149
155
|
VCR.use_cassette "fetch_assembly_aborted" do
|
|
150
|
-
@response = Transloadit::
|
|
151
|
-
RestClient::Resource.new(request_uri).get
|
|
152
|
-
).extend!(Transloadit::Response::Assembly)
|
|
156
|
+
@response = Transloadit::Request.new(request_uri).get.extend!(Transloadit::Response::Assembly)
|
|
153
157
|
end
|
|
154
158
|
|
|
155
159
|
_(@response.finished?).must_equal true
|
|
@@ -158,9 +162,7 @@ describe Transloadit::Response do
|
|
|
158
162
|
|
|
159
163
|
it "must allow to check for errors" do
|
|
160
164
|
VCR.use_cassette "fetch_assembly_errors" do
|
|
161
|
-
@response = Transloadit::
|
|
162
|
-
RestClient::Resource.new(request_uri).get
|
|
163
|
-
).extend!(Transloadit::Response::Assembly)
|
|
165
|
+
@response = Transloadit::Request.new(request_uri).get.extend!(Transloadit::Response::Assembly)
|
|
164
166
|
end
|
|
165
167
|
|
|
166
168
|
_(@response.error?).must_equal true
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "stringio"
|
|
3
|
+
require "tempfile"
|
|
4
|
+
|
|
5
|
+
describe "Faraday transport compatibility" do
|
|
6
|
+
include WebMock::API
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
WebMock.reset!
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
after do
|
|
13
|
+
WebMock.reset!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "sends signed GET parameters and the client header to a selected host" do
|
|
17
|
+
endpoint = "https://api2.example.test/assemblies/assembly-id"
|
|
18
|
+
stub_request(:get, /\A#{Regexp.escape(endpoint)}/)
|
|
19
|
+
.to_return(status: 200, body: '{"ok":"ASSEMBLY_COMPLETED"}')
|
|
20
|
+
|
|
21
|
+
response = Transloadit::Request.new(endpoint, "secret").get(wait: true)
|
|
22
|
+
|
|
23
|
+
_(response["ok"]).must_equal "ASSEMBLY_COMPLETED"
|
|
24
|
+
assert_requested(:get, /\A#{Regexp.escape(endpoint)}/) do |request|
|
|
25
|
+
query = Addressable::URI.parse(request.uri.to_s).query_values
|
|
26
|
+
_(MultiJson.load(query.fetch("params"))).must_equal "wait" => true
|
|
27
|
+
_(query.fetch("signature")).must_match(/\Asha384:[0-9a-f]{96}\z/)
|
|
28
|
+
_(request.headers.fetch("Transloadit-Client")).must_equal "ruby-sdk:#{Transloadit::VERSION}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "form-encodes signed POST payloads" do
|
|
33
|
+
endpoint = "https://api2.transloadit.com/assemblies"
|
|
34
|
+
stub_request(:post, endpoint)
|
|
35
|
+
.to_return(status: 200, body: '{"ok":"ASSEMBLY_COMPLETED"}')
|
|
36
|
+
|
|
37
|
+
Transloadit::Request.new("/assemblies", "secret").post(params: {template_id: "template-id"})
|
|
38
|
+
|
|
39
|
+
assert_requested(:post, endpoint) do |request|
|
|
40
|
+
form = URI.decode_www_form(request.body).to_h
|
|
41
|
+
_(MultiJson.load(form.fetch("params"))).must_equal "template_id" => "template-id"
|
|
42
|
+
_(form.fetch("signature")).must_match(/\Asha384:[0-9a-f]{96}\z/)
|
|
43
|
+
_(request.headers.fetch("Content-Type")).must_match(/\Aapplication\/x-www-form-urlencoded/)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "form-encodes signed PUT payloads" do
|
|
48
|
+
endpoint = "https://api2.transloadit.com/templates/template-id"
|
|
49
|
+
stub_request(:put, endpoint)
|
|
50
|
+
.to_return(status: 200, body: '{"ok":"TEMPLATE_UPDATED"}')
|
|
51
|
+
|
|
52
|
+
Transloadit::Request.new("/templates/template-id", "secret").put(params: {name: "Updated"})
|
|
53
|
+
|
|
54
|
+
assert_requested(:put, endpoint) do |request|
|
|
55
|
+
form = URI.decode_www_form(request.body).to_h
|
|
56
|
+
_(MultiJson.load(form.fetch("params"))).must_equal "name" => "Updated"
|
|
57
|
+
_(form.fetch("signature")).must_match(/\Asha384:[0-9a-f]{96}\z/)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "form-encodes signed DELETE payloads" do
|
|
62
|
+
endpoint = "https://api2.transloadit.com/templates/template-id"
|
|
63
|
+
stub_request(:delete, endpoint)
|
|
64
|
+
.to_return(status: 200, body: '{"ok":"TEMPLATE_DELETED"}')
|
|
65
|
+
|
|
66
|
+
Transloadit::Request.new("/templates/template-id", "secret").delete(params: {reason: "cleanup"})
|
|
67
|
+
|
|
68
|
+
assert_requested(:delete, endpoint) do |request|
|
|
69
|
+
form = URI.decode_www_form(request.body).to_h
|
|
70
|
+
_(MultiJson.load(form.fetch("params"))).must_equal "reason" => "cleanup"
|
|
71
|
+
_(form.fetch("signature")).must_match(/\Asha384:[0-9a-f]{96}\z/)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "returns non-success responses with SDK-owned status, headers, and body access" do
|
|
76
|
+
endpoint = "https://api2.example.test/assemblies/missing"
|
|
77
|
+
stub_request(:get, endpoint).to_return(
|
|
78
|
+
status: 422,
|
|
79
|
+
headers: {"Retry-After" => "5", "X-Request-Id" => "request-id"},
|
|
80
|
+
body: '{"error":"ASSEMBLY_NOT_FOUND"}'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
response = Transloadit::Request.new(endpoint).get
|
|
84
|
+
|
|
85
|
+
_(response).must_be_kind_of Transloadit::Response
|
|
86
|
+
_(response).wont_be_kind_of Faraday::Response
|
|
87
|
+
_(response.code).must_equal 422
|
|
88
|
+
_(response.status).must_equal 422
|
|
89
|
+
_(response.headers).must_equal retry_after: "5", x_request_id: "request-id"
|
|
90
|
+
_(response["error"]).must_equal "ASSEMBLY_NOT_FOUND"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "returns redirects without following them" do
|
|
94
|
+
endpoint = "https://api2.example.test/assemblies"
|
|
95
|
+
redirect = "https://uploads.example.test/assemblies"
|
|
96
|
+
stub_request(:post, endpoint).to_return(
|
|
97
|
+
status: 302,
|
|
98
|
+
headers: {"Location" => redirect},
|
|
99
|
+
body: "{}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
response = Transloadit::Request.new(endpoint).post
|
|
103
|
+
|
|
104
|
+
_(response.code).must_equal 302
|
|
105
|
+
_(response.headers[:location]).must_equal redirect
|
|
106
|
+
assert_not_requested(:post, redirect)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "uploads multiple files after params and signature without closing caller-owned files" do
|
|
110
|
+
endpoint = "https://api2.transloadit.com/assemblies"
|
|
111
|
+
stub_request(:post, endpoint)
|
|
112
|
+
.to_return(status: 200, body: '{"ok":"ASSEMBLY_COMPLETED"}')
|
|
113
|
+
|
|
114
|
+
Tempfile.create(["first", ".txt"]) do |first|
|
|
115
|
+
Tempfile.create(["second", ".json"]) do |second|
|
|
116
|
+
first.write("first upload")
|
|
117
|
+
first.flush
|
|
118
|
+
second.write('{"second":"upload"}')
|
|
119
|
+
second.flush
|
|
120
|
+
|
|
121
|
+
Transloadit::Request.new("/assemblies", "secret").post(
|
|
122
|
+
params: {steps: {}},
|
|
123
|
+
file_0: first,
|
|
124
|
+
file_1: second
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
_(first.closed?).must_equal false
|
|
128
|
+
_(second.closed?).must_equal false
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
assert_requested(:post, endpoint) do |request|
|
|
133
|
+
params_position = request.body.index('name="params"')
|
|
134
|
+
signature_position = request.body.index('name="signature"')
|
|
135
|
+
first_position = request.body.index('name="file_0"')
|
|
136
|
+
second_position = request.body.index('name="file_1"')
|
|
137
|
+
|
|
138
|
+
_(request.headers.fetch("Content-Type")).must_match(/\Amultipart\/form-data; boundary=/)
|
|
139
|
+
_(params_position).wont_be_nil
|
|
140
|
+
_(signature_position).wont_be_nil
|
|
141
|
+
_(first_position).wont_be_nil
|
|
142
|
+
_(second_position).wont_be_nil
|
|
143
|
+
_(params_position < signature_position).must_equal true
|
|
144
|
+
_(signature_position < first_position).must_equal true
|
|
145
|
+
_(first_position < second_position).must_equal true
|
|
146
|
+
_(request.body).must_include "Content-Type: text/plain"
|
|
147
|
+
_(request.body).must_include "Content-Type: application/json"
|
|
148
|
+
_(request.body).must_include "first upload"
|
|
149
|
+
_(request.body).must_include '{"second":"upload"}'
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "preserves filename and content type from Rails-style uploaded files" do
|
|
154
|
+
endpoint = "https://api2.transloadit.com/assemblies"
|
|
155
|
+
stub_request(:post, endpoint)
|
|
156
|
+
.to_return(status: 200, body: '{"ok":"ASSEMBLY_COMPLETED"}')
|
|
157
|
+
|
|
158
|
+
Tempfile.create(["rails-upload", ".bin"]) do |file|
|
|
159
|
+
file.write("csv contents")
|
|
160
|
+
file.flush
|
|
161
|
+
upload = Object.new
|
|
162
|
+
upload.define_singleton_method(:read) { |*arguments| file.read(*arguments) }
|
|
163
|
+
upload.define_singleton_method(:path) { file.path }
|
|
164
|
+
upload.define_singleton_method(:original_filename) { "report.csv" }
|
|
165
|
+
upload.define_singleton_method(:content_type) { "text/csv" }
|
|
166
|
+
|
|
167
|
+
Transloadit::Request.new("/assemblies").post(params: {steps: {}}, file_0: upload)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
assert_requested(:post, endpoint) do |request|
|
|
171
|
+
_(request.body).must_include 'filename="report.csv"'
|
|
172
|
+
_(request.body).must_include "Content-Type: text/csv"
|
|
173
|
+
_(request.body).must_include "csv contents"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "uses safe metadata defaults for in-memory uploads" do
|
|
178
|
+
endpoint = "https://api2.transloadit.com/assemblies"
|
|
179
|
+
stub_request(:post, endpoint)
|
|
180
|
+
.to_return(status: 200, body: '{"ok":"ASSEMBLY_COMPLETED"}')
|
|
181
|
+
upload = StringIO.new("memory upload")
|
|
182
|
+
|
|
183
|
+
Transloadit::Request.new("/assemblies").post(params: {steps: {}}, file_0: upload)
|
|
184
|
+
|
|
185
|
+
_(upload.closed?).must_equal false
|
|
186
|
+
assert_requested(:post, endpoint) do |request|
|
|
187
|
+
_(request.body).must_include 'filename="upload"'
|
|
188
|
+
_(request.body).must_include "Content-Type: application/octet-stream"
|
|
189
|
+
_(request.body).must_include "memory upload"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "uses the binary content type when a file extension is unknown" do
|
|
194
|
+
endpoint = "https://api2.transloadit.com/assemblies"
|
|
195
|
+
stub_request(:post, endpoint)
|
|
196
|
+
.to_return(status: 200, body: '{"ok":"ASSEMBLY_COMPLETED"}')
|
|
197
|
+
|
|
198
|
+
Tempfile.create(["upload", ".transloadit-unknown"]) do |upload|
|
|
199
|
+
upload.write("unknown upload")
|
|
200
|
+
upload.flush
|
|
201
|
+
|
|
202
|
+
Transloadit::Request.new("/assemblies").post(params: {steps: {}}, file_0: upload)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
assert_requested(:post, endpoint) do |request|
|
|
206
|
+
_(request.body).must_include "Content-Type: application/octet-stream"
|
|
207
|
+
_(request.body).must_include "unknown upload"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "wraps adapter failures and retains the original error as the cause" do
|
|
212
|
+
endpoint = "https://api2.example.test/assemblies/assembly-id"
|
|
213
|
+
stub_request(:get, endpoint).to_timeout
|
|
214
|
+
|
|
215
|
+
error = assert_raises Transloadit::Exception::RequestFailed do
|
|
216
|
+
Transloadit::Request.new(endpoint).get
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
_(error.message).must_equal "Transloadit request failed"
|
|
220
|
+
_(error.cause).must_be_kind_of Faraday::ConnectionFailed
|
|
221
|
+
end
|
|
222
|
+
end
|
data/transloadit.gemspec
CHANGED
|
@@ -16,21 +16,22 @@ Gem::Specification.new do |gem|
|
|
|
16
16
|
gem.description = "The transloadit gem allows you to automate uploading files through the Transloadit REST API"
|
|
17
17
|
|
|
18
18
|
gem.required_rubygems_version = ">= 2.2.0"
|
|
19
|
-
gem.required_ruby_version = ">= 3.
|
|
19
|
+
gem.required_ruby_version = ">= 3.1.0"
|
|
20
20
|
|
|
21
21
|
gem.files = `git ls-files`.split("\n")
|
|
22
22
|
gem.require_paths = %w[lib]
|
|
23
23
|
|
|
24
|
-
gem.add_dependency "
|
|
24
|
+
gem.add_dependency "faraday", ">= 2.0", "< 3.0"
|
|
25
|
+
gem.add_dependency "faraday-multipart", "~> 1.0"
|
|
25
26
|
gem.add_dependency "multi_json"
|
|
26
27
|
gem.add_dependency "mime-types"
|
|
27
28
|
|
|
28
29
|
gem.add_development_dependency "rake"
|
|
29
30
|
gem.add_development_dependency "minitest"
|
|
30
31
|
gem.add_development_dependency "simplecov"
|
|
31
|
-
gem.add_development_dependency "standard"
|
|
32
|
+
gem.add_development_dependency "standard", ">= 1.54"
|
|
32
33
|
|
|
33
|
-
gem.add_development_dependency "vcr"
|
|
34
|
+
gem.add_development_dependency "vcr", ">= 6.4"
|
|
34
35
|
gem.add_development_dependency "webmock"
|
|
35
36
|
|
|
36
37
|
gem.add_development_dependency "yard"
|