aliyun-oss-ruby-sdk 0.4.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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +95 -0
  3. data/README.md +423 -0
  4. data/examples/aliyun/oss/bucket.rb +144 -0
  5. data/examples/aliyun/oss/callback.rb +61 -0
  6. data/examples/aliyun/oss/object.rb +182 -0
  7. data/examples/aliyun/oss/resumable_download.rb +42 -0
  8. data/examples/aliyun/oss/resumable_upload.rb +49 -0
  9. data/examples/aliyun/oss/streaming.rb +124 -0
  10. data/examples/aliyun/oss/using_sts.rb +48 -0
  11. data/examples/aliyun/sts/assume_role.rb +59 -0
  12. data/lib/aliyun_sdk/common.rb +6 -0
  13. data/lib/aliyun_sdk/common/exception.rb +18 -0
  14. data/lib/aliyun_sdk/common/logging.rb +46 -0
  15. data/lib/aliyun_sdk/common/struct.rb +56 -0
  16. data/lib/aliyun_sdk/oss.rb +16 -0
  17. data/lib/aliyun_sdk/oss/bucket.rb +661 -0
  18. data/lib/aliyun_sdk/oss/client.rb +106 -0
  19. data/lib/aliyun_sdk/oss/config.rb +39 -0
  20. data/lib/aliyun_sdk/oss/download.rb +255 -0
  21. data/lib/aliyun_sdk/oss/exception.rb +108 -0
  22. data/lib/aliyun_sdk/oss/http.rb +338 -0
  23. data/lib/aliyun_sdk/oss/iterator.rb +92 -0
  24. data/lib/aliyun_sdk/oss/multipart.rb +74 -0
  25. data/lib/aliyun_sdk/oss/object.rb +15 -0
  26. data/lib/aliyun_sdk/oss/protocol.rb +1499 -0
  27. data/lib/aliyun_sdk/oss/struct.rb +208 -0
  28. data/lib/aliyun_sdk/oss/upload.rb +238 -0
  29. data/lib/aliyun_sdk/oss/util.rb +89 -0
  30. data/lib/aliyun_sdk/sts.rb +9 -0
  31. data/lib/aliyun_sdk/sts/client.rb +38 -0
  32. data/lib/aliyun_sdk/sts/config.rb +22 -0
  33. data/lib/aliyun_sdk/sts/exception.rb +53 -0
  34. data/lib/aliyun_sdk/sts/protocol.rb +130 -0
  35. data/lib/aliyun_sdk/sts/struct.rb +64 -0
  36. data/lib/aliyun_sdk/sts/util.rb +48 -0
  37. data/lib/aliyun_sdk/version.rb +7 -0
  38. data/spec/aliyun/oss/bucket_spec.rb +597 -0
  39. data/spec/aliyun/oss/client/bucket_spec.rb +554 -0
  40. data/spec/aliyun/oss/client/client_spec.rb +297 -0
  41. data/spec/aliyun/oss/client/resumable_download_spec.rb +220 -0
  42. data/spec/aliyun/oss/client/resumable_upload_spec.rb +413 -0
  43. data/spec/aliyun/oss/http_spec.rb +83 -0
  44. data/spec/aliyun/oss/multipart_spec.rb +686 -0
  45. data/spec/aliyun/oss/object_spec.rb +785 -0
  46. data/spec/aliyun/oss/service_spec.rb +142 -0
  47. data/spec/aliyun/oss/util_spec.rb +50 -0
  48. data/spec/aliyun/sts/client_spec.rb +150 -0
  49. data/spec/aliyun/sts/util_spec.rb +39 -0
  50. data/tests/config.rb +31 -0
  51. data/tests/test_content_encoding.rb +54 -0
  52. data/tests/test_content_type.rb +95 -0
  53. data/tests/test_custom_headers.rb +70 -0
  54. data/tests/test_encoding.rb +77 -0
  55. data/tests/test_large_file.rb +66 -0
  56. data/tests/test_multipart.rb +97 -0
  57. data/tests/test_object_acl.rb +49 -0
  58. data/tests/test_object_key.rb +68 -0
  59. data/tests/test_object_url.rb +69 -0
  60. data/tests/test_resumable.rb +40 -0
  61. metadata +240 -0
@@ -0,0 +1,554 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'spec_helper'
4
+ require 'yaml'
5
+ require 'nokogiri'
6
+
7
+ module AliyunSDK
8
+ module OSS
9
+
10
+ describe Bucket do
11
+
12
+ before :all do
13
+ @endpoint = 'oss-cn-hangzhou.aliyuncs.com'
14
+ @bucket_name = 'rubysdk-bucket'
15
+ @bucket = Client.new(
16
+ :endpoint => @endpoint,
17
+ :access_key_id => 'xxx',
18
+ :access_key_secret => 'yyy').get_bucket(@bucket_name)
19
+ end
20
+
21
+ def bucket_url
22
+ "#{@bucket_name}.#{@endpoint}"
23
+ end
24
+
25
+ def object_url(object)
26
+ "#{@bucket_name}.#{@endpoint}/#{object}"
27
+ end
28
+
29
+ def resource_path(object)
30
+ "/#{@bucket_name}/#{object}"
31
+ end
32
+
33
+ def mock_objects(objects, more = {})
34
+ Nokogiri::XML::Builder.new do |xml|
35
+ xml.ListBucketResult {
36
+ {
37
+ :prefix => 'Prefix',
38
+ :delimiter => 'Delimiter',
39
+ :limit => 'MaxKeys',
40
+ :marker => 'Marker',
41
+ :next_marker => 'NextMarker',
42
+ :truncated => 'IsTruncated',
43
+ :encoding => 'EncodingType'
44
+ }.map do |k, v|
45
+ xml.send(v, more[k]) if more[k] != nil
46
+ end
47
+
48
+ objects.each do |o|
49
+ xml.Contents {
50
+ xml.Key o.key
51
+ xml.Size o.size
52
+ xml.ETag o.etag
53
+ }
54
+ end
55
+
56
+ (more[:common_prefixes] || []).each do |p|
57
+ xml.CommonPrefixes {
58
+ xml.Prefix p
59
+ }
60
+ end
61
+ }
62
+ end.to_xml
63
+ end
64
+
65
+ def mock_uploads(txns, more = {})
66
+ Nokogiri::XML::Builder.new do |xml|
67
+ xml.ListMultipartUploadsResult {
68
+ {
69
+ :prefix => 'Prefix',
70
+ :delimiter => 'Delimiter',
71
+ :limit => 'MaxUploads',
72
+ :key_marker => 'KeyMarker',
73
+ :id_marker => 'UploadIdMarker',
74
+ :next_key_marker => 'NextKeyMarker',
75
+ :next_id_marker => 'NextUploadIdMarker',
76
+ :truncated => 'IsTruncated',
77
+ :encoding => 'EncodingType'
78
+ }.map do |k, v|
79
+ xml.send(v, more[k]) if more[k] != nil
80
+ end
81
+
82
+ txns.each do |t|
83
+ xml.Upload {
84
+ xml.Key t.object
85
+ xml.UploadId t.id
86
+ }
87
+ end
88
+ }
89
+ end.to_xml
90
+ end
91
+
92
+ def mock_acl(acl)
93
+ Nokogiri::XML::Builder.new do |xml|
94
+ xml.AccessControlPolicy {
95
+ xml.Owner {
96
+ xml.ID 'owner_id'
97
+ xml.DisplayName 'owner_name'
98
+ }
99
+
100
+ xml.AccessControlList {
101
+ xml.Grant acl
102
+ }
103
+ }
104
+ end.to_xml
105
+ end
106
+
107
+ def mock_error(code, message)
108
+ Nokogiri::XML::Builder.new do |xml|
109
+ xml.Error {
110
+ xml.Code code
111
+ xml.Message message
112
+ xml.RequestId '0000'
113
+ }
114
+ end.to_xml
115
+ end
116
+
117
+ def err(msg, reqid = '0000')
118
+ "#{msg} RequestId: #{reqid}"
119
+ end
120
+
121
+ context "bucket operations" do
122
+ it "should get acl" do
123
+ query = {'acl' => ''}
124
+ return_acl = ACL::PUBLIC_READ
125
+
126
+ stub_request(:get, bucket_url)
127
+ .with(:query => query)
128
+ .to_return(:body => mock_acl(return_acl))
129
+
130
+ acl = @bucket.acl
131
+
132
+ expect(WebMock).to have_requested(:get, bucket_url)
133
+ .with(:query => query, :body => nil)
134
+ expect(acl).to eq(return_acl)
135
+ end
136
+
137
+ it "should set acl" do
138
+ query = {'acl' => ''}
139
+
140
+ stub_request(:put, bucket_url).with(:query => query)
141
+
142
+ @bucket.acl = ACL::PUBLIC_READ
143
+
144
+ expect(WebMock).to have_requested(:put, bucket_url)
145
+ .with(:query => query, :body => nil)
146
+ end
147
+
148
+ it "should delete logging setting" do
149
+ query = {'logging' => ''}
150
+
151
+ stub_request(:delete, bucket_url).with(:query => query)
152
+
153
+ @bucket.logging = BucketLogging.new(:enable => false)
154
+
155
+ expect(WebMock).to have_requested(:delete, bucket_url)
156
+ .with(:query => query, :body => nil)
157
+ end
158
+
159
+ it "should get bucket url" do
160
+ expect(@bucket.bucket_url)
161
+ .to eq('http://rubysdk-bucket.oss-cn-hangzhou.aliyuncs.com/')
162
+ end
163
+
164
+ it "should get access key id" do
165
+ expect(@bucket.access_key_id).to eq('xxx')
166
+ end
167
+ end # bucket operations
168
+
169
+ context "object operations" do
170
+ it "should list objects" do
171
+ query_1 = {
172
+ :prefix => 'list-',
173
+ :delimiter => '-',
174
+ 'encoding-type' => 'url'
175
+ }
176
+ return_obj_1 = (1..5).map{ |i| Object.new(
177
+ :key => "obj-#{i}",
178
+ :size => 1024 * i,
179
+ :etag => "etag-#{i}")}
180
+ return_more_1 = {
181
+ :next_marker => 'foo',
182
+ :truncated => true,
183
+ :common_prefixes => ['hello', 'world']
184
+ }
185
+
186
+ query_2 = {
187
+ :prefix => 'list-',
188
+ :delimiter => '-',
189
+ :marker => 'foo',
190
+ 'encoding-type' => 'url'
191
+ }
192
+ return_obj_2 = (6..8).map{ |i| Object.new(
193
+ :key => "obj-#{i}",
194
+ :size => 1024 * i,
195
+ :etag => "etag-#{i}")}
196
+ return_more_2 = {
197
+ :next_marker => 'bar',
198
+ :truncated => false,
199
+ :common_prefixes => ['rock', 'suck']
200
+ }
201
+
202
+ stub_request(:get, bucket_url)
203
+ .with(:query => query_1)
204
+ .to_return(:body => mock_objects(return_obj_1, return_more_1))
205
+
206
+ stub_request(:get, bucket_url)
207
+ .with(:query => query_2)
208
+ .to_return(:body => mock_objects(return_obj_2, return_more_2))
209
+
210
+ list = @bucket.list_objects :prefix => 'list-', :delimiter => '-'
211
+ all = list.to_a
212
+
213
+ expect(WebMock).to have_requested(:get, bucket_url)
214
+ .with(:query => query_1).times(1)
215
+ expect(WebMock).to have_requested(:get, bucket_url)
216
+ .with(:query => query_2).times(1)
217
+
218
+ objs = all.select{ |x| x.is_a?(Object) }
219
+ common_prefixes = all.select{ |x| x.is_a?(String) }
220
+ all_objs = (1..8).map{ |i| Object.new(
221
+ :key => "obj-#{i}",
222
+ :size => 1024 * i,
223
+ :etag => "etag-#{i}")}
224
+ expect(objs.map{ |o| o.to_s }).to match_array(all_objs.map{ |o| o.to_s })
225
+ all_prefixes = ['hello', 'world', 'rock', 'suck']
226
+ expect(common_prefixes).to match_array(all_prefixes)
227
+ end
228
+
229
+ it "should put object from file" do
230
+ key = 'ruby'
231
+ stub_request(:put, object_url(key))
232
+
233
+ content = (1..10).map{ |i| i.to_s.rjust(9, '0') }.join("\n")
234
+ File.open('/tmp/x', 'w'){ |f| f.write(content) }
235
+
236
+ @bucket.put_object(key, :file => '/tmp/x')
237
+
238
+ expect(WebMock).to have_requested(:put, object_url(key))
239
+ .with(:body => content, :query => {})
240
+ end
241
+
242
+ it "should put object with acl" do
243
+ key = 'ruby'
244
+ stub_request(:put, object_url(key))
245
+
246
+ @bucket.put_object(key, :acl => ACL::PUBLIC_READ)
247
+
248
+ expect(WebMock)
249
+ .to have_requested(:put, object_url(key))
250
+ .with(:headers => {'X-Oss-Object-Acl' => ACL::PUBLIC_READ})
251
+ end
252
+
253
+ it "should put object with callback" do
254
+ key = 'ruby'
255
+ stub_request(:put, object_url(key))
256
+
257
+ callback = Callback.new(
258
+ url: 'http://app.server.com/callback',
259
+ query: {'id' => 1, 'name' => '杭州'},
260
+ body: 'hello world',
261
+ host: 'server.com'
262
+ )
263
+ @bucket.put_object(key, callback: callback)
264
+
265
+ expect(WebMock).to have_requested(:put, object_url(key))
266
+ .with { |req| req.headers.key?('X-Oss-Callback') }
267
+ end
268
+
269
+ it "should raise CallbackError when callback failed" do
270
+ key = 'ruby'
271
+ code = 'CallbackFailed'
272
+ message = 'Error status: 502.'
273
+ stub_request(:put, object_url(key))
274
+ .to_return(:status => 203, :body => mock_error(code, message))
275
+
276
+ callback = Callback.new(
277
+ url: 'http://app.server.com/callback',
278
+ query: {'id' => 1, 'name' => '杭州'},
279
+ body: 'hello world',
280
+ host: 'server.com'
281
+ )
282
+ expect {
283
+ @bucket.put_object(key, callback: callback)
284
+ }.to raise_error(CallbackError, err(message))
285
+
286
+ expect(WebMock).to have_requested(:put, object_url(key))
287
+ .with { |req| req.headers.key?('X-Oss-Callback') }
288
+ end
289
+
290
+ it "should set custom headers when put object" do
291
+ key = 'ruby'
292
+ stub_request(:put, object_url(key))
293
+
294
+ @bucket.put_object(
295
+ key, headers: {'cache-control' => 'xxx', 'expires' => 'yyy'})
296
+
297
+ headers = {}
298
+ expect(WebMock).to have_requested(:put, object_url(key))
299
+ .with { |req| headers = req.headers }
300
+ expect(headers['Cache-Control']).to eq('xxx')
301
+ expect(headers['Expires']).to eq('yyy')
302
+ end
303
+
304
+ it "should set custom headers when append object" do
305
+ key = 'ruby'
306
+ query = {'append' => '', 'position' => 11}
307
+ stub_request(:post, object_url(key)).with(:query => query)
308
+
309
+ @bucket.append_object(
310
+ key, 11,
311
+ headers: {'CACHE-CONTROL' => 'nocache', 'EXPIRES' => 'seripxe'})
312
+
313
+ headers = {}
314
+ expect(WebMock).to have_requested(:post, object_url(key))
315
+ .with(:query => query)
316
+ .with { |req| headers = req.headers }
317
+ expect(headers['Cache-Control']).to eq('nocache')
318
+ expect(headers['Expires']).to eq('seripxe')
319
+ end
320
+
321
+ it "should get object to file" do
322
+ key = 'ruby'
323
+ # 100 KB
324
+ content = (1..1024).map{ |i| i.to_s.rjust(99, '0') }.join(",")
325
+
326
+ stub_request(:get, object_url(key)).to_return(:body => content)
327
+
328
+ @bucket.get_object(key, :file => '/tmp/x')
329
+
330
+ expect(WebMock).to have_requested(:get, object_url(key))
331
+ .with(:body => nil, :query => {})
332
+ expect(File.read('/tmp/x')).to eq(content)
333
+ end
334
+
335
+ it "should only get meta when get object without :file or block" do
336
+ key = 'ruby'
337
+
338
+ last_modified = Time.now.rfc822
339
+ return_headers = {
340
+ 'x-oss-object-type' => 'Normal',
341
+ 'ETag' => 'xxxyyyzzz',
342
+ 'Content-Length' => 1024,
343
+ 'Last-Modified' => last_modified,
344
+ 'x-oss-meta-year' => '2015',
345
+ 'x-oss-meta-people' => 'mary'
346
+ }
347
+ stub_request(:head, object_url(key))
348
+ .to_return(:headers => return_headers)
349
+
350
+ obj = @bucket.get_object(key)
351
+
352
+ expect(WebMock).to have_requested(:head, object_url(key))
353
+ .with(:body => nil, :query => {})
354
+
355
+ expect(obj.key).to eq(key)
356
+ expect(obj.type).to eq('Normal')
357
+ expect(obj.etag).to eq('xxxyyyzzz')
358
+ expect(obj.size).to eq(1024)
359
+ expect(obj.last_modified.rfc822).to eq(last_modified)
360
+ expect(obj.metas).to eq({'year' => '2015', 'people' => 'mary'})
361
+ end
362
+
363
+ it "should append object from file" do
364
+ key = 'ruby'
365
+ query = {'append' => '', 'position' => 11}
366
+ stub_request(:post, object_url(key)).with(:query => query)
367
+
368
+ content = (1..10).map{ |i| i.to_s.rjust(9, '0') }.join("\n")
369
+ content = "<html>" + content + "</html>"
370
+ File.open('/tmp/x.html', 'w'){ |f| f.write(content) }
371
+
372
+ @bucket.append_object(key, 11, :file => '/tmp/x.html')
373
+
374
+ expect(WebMock).to have_requested(:post, object_url(key))
375
+ .with(:query => query, :body => content,
376
+ :headers => {'Content-Type' => 'text/html'})
377
+ end
378
+
379
+ it "should append object with acl" do
380
+ key = 'ruby'
381
+ query = {'append' => '', 'position' => 11}
382
+ stub_request(:post, object_url(key)).with(:query => query)
383
+
384
+ @bucket.append_object(key, 11, :acl => ACL::PUBLIC_READ_WRITE)
385
+
386
+ expect(WebMock)
387
+ .to have_requested(:post, object_url(key))
388
+ .with(:query => query,
389
+ :headers => {'X-Oss-Object-Acl' => ACL::PUBLIC_READ_WRITE})
390
+ end
391
+
392
+ it "should answer object exists?" do
393
+ key = 'ruby'
394
+
395
+ stub_request(:head, object_url(key))
396
+ .to_return(:status => 404).times(3)
397
+
398
+ expect {
399
+ @bucket.get_object(key)
400
+ }.to raise_error(ServerError, err("UnknownError[404].", ''))
401
+
402
+ expect(@bucket.object_exists?(key)).to be false
403
+ expect(@bucket.object_exist?(key)).to be false
404
+
405
+ last_modified = Time.now.rfc822
406
+ return_headers = {
407
+ 'x-oss-object-type' => 'Normal',
408
+ 'ETag' => 'xxxyyyzzz',
409
+ 'Content-Length' => 1024,
410
+ 'Last-Modified' => last_modified,
411
+ 'x-oss-meta-year' => '2015',
412
+ 'x-oss-meta-people' => 'mary'
413
+ }
414
+
415
+ stub_request(:head, object_url(key))
416
+ .to_return(:headers => return_headers).times(2)
417
+
418
+ expect(@bucket.object_exists?(key)).to be true
419
+ expect(@bucket.object_exist?(key)).to be true
420
+
421
+ stub_request(:head, object_url(key))
422
+ .to_return(:status => 500)
423
+
424
+ expect {
425
+ @bucket.object_exists?(key)
426
+ }.to raise_error(ServerError, err("UnknownError[500].", ''))
427
+ end
428
+
429
+ it "should update object metas" do
430
+ key = 'ruby'
431
+
432
+ stub_request(:put, object_url(key))
433
+
434
+ @bucket.update_object_metas(
435
+ key, {'people' => 'mary', 'year' => '2016'})
436
+
437
+ expect(WebMock).to have_requested(:put, object_url(key))
438
+ .with(:body => nil,
439
+ :headers => {
440
+ 'x-oss-copy-source' => resource_path(key),
441
+ 'x-oss-metadata-directive' => 'REPLACE',
442
+ 'x-oss-meta-year' => '2016',
443
+ 'x-oss-meta-people' => 'mary'})
444
+ end
445
+
446
+ it "should get object url" do
447
+ url = @bucket.object_url('yeah', false)
448
+ object_url = 'http://rubysdk-bucket.oss-cn-hangzhou.aliyuncs.com/yeah'
449
+ expect(url).to eq(object_url)
450
+
451
+ url = @bucket.object_url('yeah')
452
+ path = url[0, url.index('?')]
453
+ expect(path).to eq(object_url)
454
+
455
+ query = {}
456
+ url[url.index('?') + 1, url.size].split('&')
457
+ .each { |s| k, v = s.split('='); query[k] = v }
458
+
459
+ expect(query.key?('Expires')).to be true
460
+ expect(query['OSSAccessKeyId']).to eq('xxx')
461
+ expires = query['Expires']
462
+ signature = CGI.unescape(query['Signature'])
463
+
464
+ string_to_sign =
465
+ "GET\n" + "\n\n" + "#{expires}\n" + "/rubysdk-bucket/yeah"
466
+ sig = Util.sign('yyy', string_to_sign)
467
+ expect(signature).to eq(sig)
468
+ end
469
+
470
+ it "should get object url with STS" do
471
+ sts_bucket = Client.new(
472
+ :endpoint => @endpoint,
473
+ :access_key_id => 'xxx',
474
+ :access_key_secret => 'yyy',
475
+ :sts_token => 'zzz').get_bucket(@bucket_name)
476
+
477
+ object_url = 'http://rubysdk-bucket.oss-cn-hangzhou.aliyuncs.com/yeah'
478
+
479
+ url = sts_bucket.object_url('yeah')
480
+ path = url[0, url.index('?')]
481
+ expect(path).to eq(object_url)
482
+
483
+ query = {}
484
+ url[url.index('?') + 1, url.size].split('&')
485
+ .each { |s| k, v = s.split('='); query[k] = v }
486
+
487
+ expect(query.key?('Expires')).to be true
488
+ expect(query.key?('Signature')).to be true
489
+ expect(query['OSSAccessKeyId']).to eq('xxx')
490
+ expect(query['security-token']).to eq('zzz')
491
+ end
492
+
493
+ end # object operations
494
+
495
+ context "multipart operations" do
496
+ it "should list uploads" do
497
+ query_1 = {
498
+ :prefix => 'list-',
499
+ 'encoding-type' => 'url',
500
+ 'uploads' => ''
501
+ }
502
+ return_up_1 = (1..5).map{ |i| Multipart::Transaction.new(
503
+ :id => "txn-#{i}",
504
+ :object => "my-object",
505
+ :bucket => @bucket_name
506
+ )}
507
+ return_more_1 = {
508
+ :next_id_marker => "txn-5",
509
+ :truncated => true
510
+ }
511
+
512
+ query_2 = {
513
+ :prefix => 'list-',
514
+ 'upload-id-marker' => 'txn-5',
515
+ 'encoding-type' => 'url',
516
+ 'uploads' => ''
517
+ }
518
+ return_up_2 = (6..8).map{ |i| Multipart::Transaction.new(
519
+ :id => "txn-#{i}",
520
+ :object => "my-object",
521
+ :bucket => @bucket_name
522
+ )}
523
+ return_more_2 = {
524
+ :next_id_marker => 'txn-8',
525
+ :truncated => false,
526
+ }
527
+
528
+ stub_request(:get, bucket_url)
529
+ .with(:query => query_1)
530
+ .to_return(:body => mock_uploads(return_up_1, return_more_1))
531
+
532
+ stub_request(:get, bucket_url)
533
+ .with(:query => query_2)
534
+ .to_return(:body => mock_uploads(return_up_2, return_more_2))
535
+
536
+ txns = @bucket.list_uploads(prefix: 'list-').to_a
537
+
538
+ expect(WebMock).to have_requested(:get, bucket_url)
539
+ .with(:query => query_1).times(1)
540
+ expect(WebMock).to have_requested(:get, bucket_url)
541
+ .with(:query => query_2).times(1)
542
+
543
+ all_txns = (1..8).map{ |i| Multipart::Transaction.new(
544
+ :id => "txn-#{i}",
545
+ :object => "my-object",
546
+ :bucket => @bucket_name
547
+ )}
548
+ expect(txns.map(&:to_s)).to match_array(all_txns.map(&:to_s))
549
+ end
550
+ end # multipart operations
551
+
552
+ end # Bucket
553
+ end # OSS
554
+ end # Aliyun