s3 0.2.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.
@@ -0,0 +1,110 @@
1
+ module Stree
2
+ class Service
3
+ include Parser
4
+ extend Roxy::Moxie
5
+
6
+ attr_reader :access_key_id, :secret_access_key, :use_ssl
7
+
8
+ # Compares service to other, by access_key_id and secret_access_key
9
+ def ==(other)
10
+ self.access_key_id == other.access_key_id and self.secret_access_key == other.secret_access_key
11
+ end
12
+
13
+ # ==== Parameters:
14
+ # +options+:: a hash of options described below
15
+ #
16
+ # ==== Options:
17
+ # +access_key_id+:: Amazon access key id, required
18
+ # +secret_access_key+:: Amazon secret access key, required
19
+ # +use_ssl+:: true if use ssl in connection, otherwise false
20
+ # +timeout+:: parameter for Net::HTTP module
21
+ # +debug+:: prints the raw requests to STDOUT
22
+ def initialize(options)
23
+ @access_key_id = options[:access_key_id] or raise ArgumentError, "No access key id given"
24
+ @secret_access_key = options[:secret_access_key] or raise ArgumentError, "No secret access key given"
25
+ @use_ssl = options[:use_ssl]
26
+ @timeout = options[:timeout]
27
+ @debug = options[:debug]
28
+ end
29
+
30
+ # Returns all buckets in the service and caches the result (see reload)
31
+ def buckets(reload = false)
32
+ if reload or @buckets.nil?
33
+ @buckets = list_all_my_buckets
34
+ else
35
+ @buckets
36
+ end
37
+ end
38
+
39
+ # Returns "http://" or "https://", depends on use_ssl value from initializer
40
+ def protocol
41
+ use_ssl ? "https://" : "http://"
42
+ end
43
+
44
+ # Return 443 or 80, depends on use_ssl value from initializer
45
+ def port
46
+ use_ssl ? 443 : 80
47
+ end
48
+
49
+ proxy :buckets do
50
+ # Builds new bucket with given name
51
+ def build(name)
52
+ Bucket.send(:new, proxy_owner, name)
53
+ end
54
+
55
+ # Finds the bucket with given name
56
+ def find_first(name)
57
+ bucket = build(name)
58
+ bucket.retrieve
59
+ end
60
+ alias :find :find_first
61
+
62
+ # Find all buckets in the service
63
+ def find_all
64
+ proxy_target
65
+ end
66
+
67
+ # Reloads the bucket list (clears the cache)
68
+ def reload
69
+ proxy_owner.buckets(true)
70
+ end
71
+
72
+ # Destroy all buckets in the service. Doesn't destroy non-empty
73
+ # buckets by default, pass true to force destroy (USE WITH
74
+ # CARE!).
75
+ def destroy_all(force = false)
76
+ proxy_target.each do |bucket|
77
+ bucket.destroy(force)
78
+ end
79
+ end
80
+ end
81
+
82
+ def inspect #:nodoc:
83
+ "#<#{self.class}:#@access_key_id>"
84
+ end
85
+
86
+ private
87
+
88
+ def list_all_my_buckets
89
+ response = service_request(:get)
90
+ names = parse_list_all_my_buckets_result(response.body)
91
+ names.map { |name| Bucket.send(:new, self, name) }
92
+ end
93
+
94
+ def service_request(method, options = {})
95
+ connection.request(method, options.merge(:path => "/#{options[:path]}"))
96
+ end
97
+
98
+ def connection
99
+ if @connection.nil?
100
+ @connection = Connection.new
101
+ @connection.access_key_id = @access_key_id
102
+ @connection.secret_access_key = @secret_access_key
103
+ @connection.use_ssl = @use_ssl
104
+ @connection.timeout = @timeout
105
+ @connection.debug = @debug
106
+ end
107
+ @connection
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,157 @@
1
+ module Stree
2
+
3
+ # Class responsible for generating signatures to requests.
4
+ #
5
+ # Implements algorithm defined by Amazon Web Services to sign
6
+ # request with secret private credentials
7
+ #
8
+ # === See:
9
+ # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?RESTAuthentication.html
10
+
11
+ class Signature
12
+
13
+ # Generates signature for given parameters
14
+ #
15
+ # ==== Parameters:
16
+ # +options+: a hash that contains options listed below
17
+ #
18
+ # ==== Options:
19
+ # +host+: hostname
20
+ # +request+: Net::HTTPRequest object with correct headers
21
+ # +access_key_id+: access key id
22
+ # +secret_access_key+: secret access key
23
+ #
24
+ # ==== Returns:
25
+ # Generated signature for given hostname and request
26
+ def self.generate(options)
27
+ request = options[:request]
28
+ host = options[:host]
29
+ access_key_id = options[:access_key_id]
30
+ secret_access_key = options[:secret_access_key]
31
+
32
+ http_verb = request.method
33
+ content_md5 = request["content-md5"] || ""
34
+ content_type = request["content-type"] || ""
35
+ date = request["x-amz-date"].nil? ? request["date"] : ""
36
+ canonicalized_resource = canonicalized_resource(host, request)
37
+ canonicalized_amz_headers = canonicalized_amz_headers(request)
38
+
39
+ string_to_sign = ""
40
+ string_to_sign << http_verb
41
+ string_to_sign << "\n"
42
+ string_to_sign << content_md5
43
+ string_to_sign << "\n"
44
+ string_to_sign << content_type
45
+ string_to_sign << "\n"
46
+ string_to_sign << date
47
+ string_to_sign << "\n"
48
+ string_to_sign << canonicalized_amz_headers
49
+ string_to_sign << canonicalized_resource
50
+
51
+ digest = OpenSSL::Digest::Digest.new('sha1')
52
+ hmac = OpenSSL::HMAC.digest(digest, secret_access_key, string_to_sign)
53
+ base64 = Base64.encode64(hmac)
54
+ signature = base64.chomp
55
+
56
+ "AWS #{access_key_id}:#{signature}"
57
+ end
58
+
59
+ private
60
+
61
+ # Helper method for extracting header fields from Net::HTTPRequest and
62
+ # preparing them for singing in #generate method
63
+ #
64
+ # ==== Parameters:
65
+ # +request+: Net::HTTPRequest object with header fields filled in
66
+ #
67
+ # ==== Returns:
68
+ # String containing interesting header fields in suitable order and form
69
+ def self.canonicalized_amz_headers(request)
70
+ headers = []
71
+
72
+ # 1. Convert each HTTP header name to lower-case. For example,
73
+ # 'X-Amz-Date' becomes 'x-amz-date'.
74
+ request.each { |key, value| headers << [key.downcase, value] if key =~ /\Ax-amz-/io }
75
+ #=> [["c", 0], ["a", 1], ["a", 2], ["b", 3]]
76
+
77
+ # 2. Sort the collection of headers lexicographically by header
78
+ # name.
79
+ headers.sort!
80
+ #=> [["a", 1], ["a", 2], ["b", 3], ["c", 0]]
81
+
82
+ # 3. Combine header fields with the same name into one
83
+ # "header-name:comma-separated-value-list" pair as prescribed by
84
+ # RFC 2616, section 4.2, without any white-space between
85
+ # values. For example, the two metadata headers
86
+ # 'x-amz-meta-username: fred' and 'x-amz-meta-username: barney'
87
+ # would be combined into the single header 'x-amz-meta-username:
88
+ # fred,barney'.
89
+ groupped_headers = headers.group_by { |i| i.first }
90
+ #=> {"a"=>[["a", 1], ["a", 2]], "b"=>[["b", 3]], "c"=>[["c", 0]]}
91
+ combined_headers = groupped_headers.map do |key, value|
92
+ values = value.map { |e| e.last }
93
+ [key, values.join(",")]
94
+ end
95
+ #=> [["a", "1,2"], ["b", "3"], ["c", "0"]]
96
+
97
+ # 4. "Un-fold" long headers that span multiple lines (as allowed
98
+ # by RFC 2616, section 4.2) by replacing the folding white-space
99
+ # (including new-line) by a single space.
100
+ unfolded_headers = combined_headers.map do |header|
101
+ key = header.first
102
+ value = header.last
103
+ value.gsub!(/\s+/, " ")
104
+ [key, value]
105
+ end
106
+
107
+ # 5. Trim any white-space around the colon in the header. For
108
+ # example, the header 'x-amz-meta-username: fred,barney' would
109
+ # become 'x-amz-meta-username:fred,barney'
110
+ joined_headers = unfolded_headers.map do |header|
111
+ key = header.first.strip
112
+ value = header.last.strip
113
+ "#{key}:#{value}"
114
+ end
115
+
116
+ # 6. Finally, append a new-line (U+000A) to each canonicalized
117
+ # header in the resulting list. Construct the
118
+ # CanonicalizedResource element by concatenating all headers in
119
+ # this list into a single string.
120
+ joined_headers << "" unless joined_headers.empty?
121
+ joined_headers.join("\n")
122
+ end
123
+
124
+ # Helper methods for extracting caninocalized resource address
125
+ #
126
+ # ==== Parameters:
127
+ # +host+: hostname
128
+ # +request+: Net::HTTPRequest object with headers filealds filled in
129
+ #
130
+ # ==== Returns:
131
+ # String containing extracted canonicalized resource
132
+ def self.canonicalized_resource(host, request)
133
+ # 1. Start with the empty string ("").
134
+ string = ""
135
+
136
+ # 2. If the request specifies a bucket using the HTTP Host
137
+ # header (virtual hosted-style), append the bucket name preceded
138
+ # by a "/" (e.g., "/bucketname"). For path-style requests and
139
+ # requests that don't address a bucket, do nothing. For more
140
+ # information on virtual hosted-style requests, see Virtual
141
+ # Hosting of Buckets.
142
+ bucket_name = host.sub(/\.?s3\.amazonaws\.com\Z/, "")
143
+ string << "/#{bucket_name}" unless bucket_name.empty?
144
+
145
+ # 3. Append the path part of the un-decoded HTTP Request-URI,
146
+ # up-to but not including the query string.
147
+ uri = URI.parse(request.path)
148
+ string << uri.path
149
+
150
+ # 4. If the request addresses a sub-resource, like ?location,
151
+ # ?acl, or ?torrent, append the sub-resource including question
152
+ # mark.
153
+ string << "?#{$1}" if uri.query =~ /&?(acl|torrent|logging|location)(?:&|=|\Z)/
154
+ string
155
+ end
156
+ end
157
+ end
data/lib/stree.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "base64"
2
+ require "digest/md5"
3
+ require "forwardable"
4
+ require "net/http"
5
+ require "net/https"
6
+ require "openssl"
7
+ require "rexml/document"
8
+ require "time"
9
+
10
+ require "stree/roxy/moxie"
11
+ require "stree/roxy/proxy"
12
+
13
+ require "stree/parser"
14
+ require "stree/bucket"
15
+ require "stree/connection"
16
+ require "stree/exceptions"
17
+ require "stree/object"
18
+ require "stree/service"
19
+ require "stree/signature"
20
+
21
+ module Stree
22
+ # Default (and only) host serving S3 stuff
23
+ HOST = "s3.amazonaws.com"
24
+ end
@@ -0,0 +1,231 @@
1
+ require 'test_helper'
2
+
3
+ class BucketTest < Test::Unit::TestCase
4
+ def setup
5
+ @bucket_vhost = Stree::Bucket.new(nil, "data-bucket")
6
+ @bucket_path = Stree::Bucket.new(nil, "data_bucket")
7
+ @bucket = @bucket_vhost
8
+
9
+ @response_location = Net::HTTPOK.new("1.1", "200", "OK")
10
+ stub(@response_location).body { @bucket_location_body }
11
+ @bucket_location = "EU"
12
+ @bucket_location_body = <<-EOLocation
13
+ <?xml version="1.0" encoding="UTF-8"?>\n<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">EU</LocationConstraint>
14
+ EOLocation
15
+
16
+ @reponse_owned_by_you = Net::HTTPConflict.new("1.1", "409", "Conflict")
17
+ stub(@reponse_owned_by_you).body { @bucket_owned_by_you_body }
18
+ @bucket_owned_by_you_body = <<-EOOwnedByYou
19
+ <?xml version="1.0" encoding="UTF-8"?>\n<Error> <Code>BucketAlreadyOwnedByYou</Code> <Message>Your previous request to create the named bucket succeeded and you already own it.</Message> <BucketName>bucket</BucketName> <RequestId>117D08EA0EC6E860</RequestId> <HostId>4VpMSvmJ+G5+DLtVox6O5cZNgdPlYcjCu3l0n4HjDe01vPxxuk5eTAtcAkUynRyV</HostId> </Error>
20
+ EOOwnedByYou
21
+
22
+ @reponse_already_exists = Net::HTTPConflict.new("1.1", "409", "Conflict")
23
+ stub(@response_already_exists).body { @bucket_already_exists_body }
24
+ @bucket_already_exists_body = <<-EOAlreadyExists
25
+ <?xml version="1.0" encoding="UTF-8"?>\n<Error> <Code>BucketAlreadyExists</Code> <Message>The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again.</Message> <BucketName>bucket</BucketName> <RequestId>4C154D32807C92BD</RequestId> <HostId>/xyHQgXcUXTZQhoO+NUBzbaxbFrIhKlyuaRHFnmcId0bMePvY9Zwg+dyk2LYE4g5</HostId> </Error>
26
+ EOAlreadyExists
27
+
28
+ @objects_list_empty = []
29
+ @objects_list = [
30
+ Stree::Object.new(@bucket, "obj1"),
31
+ Stree::Object.new(@bucket, "obj2")
32
+ ]
33
+
34
+ @response_objects_list_empty = Net::HTTPOK.new("1.1", "200", "OK")
35
+ stub(@response_objects_list_empty).body { @response_objects_list_empty_body }
36
+ @response_objects_list_empty_body = <<-EOEmpty
37
+ <?xml version="1.0" encoding="UTF-8"?>\n<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <Name>bucket</Name> <Prefix></Prefix> <Marker></Marker> <MaxKeys>1000</MaxKeys> <IsTruncated>false</IsTruncated> </ListBucketResult>
38
+ EOEmpty
39
+
40
+ @response_objects_list = Net::HTTPOK.new("1.1", "200", "OK")
41
+ stub(@response_objects_list).body { @response_objects_list_body }
42
+ @response_objects_list_body = <<-EOObjects
43
+ <?xml version="1.0" encoding="UTF-8"?>\n<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <Name>bucket</Name> <Prefix></Prefix> <Marker></Marker> <MaxKeys>1000</MaxKeys> <IsTruncated>false</IsTruncated> <Contents> <Key>obj1</Key> <LastModified>2009-07-03T10:17:33.000Z</LastModified> <ETag>&quot;99519cdf14c255e580e1b7bca85a458c&quot;</ETag> <Size>1729</Size> <Owner> <ID>df864aeb6f42be43f1d9e60aaabe3f15e245b035a4b79d1cfe36c4deaec67205</ID> <DisplayName>owner</DisplayName> </Owner> <StorageClass>STANDARD</StorageClass> </Contents> <Contents> <Key>obj2</Key> <LastModified>2009-07-03T11:17:33.000Z</LastModified> <ETag>&quot;99519cdf14c255e586e1b12bca85a458c&quot;</ETag> <Size>179</Size> <Owner> <ID>df864aeb6f42be43f1d9e60aaabe3f17e247b037a4b79d1cfe36c4deaec67205</ID> <DisplayName>owner</DisplayName> </Owner> <StorageClass>STANDARD</StorageClass> </Contents> </ListBucketResult>
44
+ EOObjects
45
+ end
46
+
47
+ def test_name_valid
48
+ assert_raise ArgumentError do Stree::Bucket.new(nil, "") end # should not be valid with empty name
49
+ assert_raise ArgumentError do Stree::Bucket.new(nil, "10.0.0.1") end # should not be valid with IP as name
50
+ assert_raise ArgumentError do Stree::Bucket.new(nil, "as") end # should not be valid with name shorter than 3 characters
51
+ assert_raise ArgumentError do Stree::Bucket.new(nil, "a"*256) end # should not be valid with name longer than 255 characters
52
+ assert_raise ArgumentError do Stree::Bucket.new(nil, ".asdf") end # should not allow special characters as first character
53
+ assert_raise ArgumentError do Stree::Bucket.new(nil, "-asdf") end # should not allow special characters as first character
54
+ assert_raise ArgumentError do Stree::Bucket.new(nil, "_asdf") end # should not allow special characters as first character
55
+
56
+ assert_nothing_raised do
57
+ Stree::Bucket.new(nil, "a-a-")
58
+ Stree::Bucket.new(nil, "a.a.")
59
+ Stree::Bucket.new(nil, "a_a_")
60
+ end
61
+ end
62
+
63
+ def test_path_prefix
64
+ expected = ""
65
+ actual = @bucket_vhost.path_prefix
66
+ assert_equal expected, actual
67
+
68
+ expected = "data_bucket/"
69
+ actual = @bucket_path.path_prefix
70
+ assert_equal expected, actual
71
+ end
72
+
73
+ def test_host
74
+ expected = "data-bucket.s3.amazonaws.com"
75
+ actual = @bucket_vhost.host
76
+ assert_equal expected, actual
77
+
78
+ expected = "s3.amazonaws.com"
79
+ actual = @bucket_path.host
80
+ assert_equal expected, actual
81
+ end
82
+
83
+ def test_vhost
84
+ assert @bucket_vhost.vhost?
85
+ assert ! @bucket_path.vhost?
86
+ end
87
+
88
+ def test_exists
89
+ mock(@bucket).retrieve { @bucket_vhost }
90
+ assert @bucket.exists?
91
+
92
+ mock(@bucket).retrieve { raise Stree::Error::NoSuchBucket.new(nil, nil) }
93
+ assert ! @bucket.exists?
94
+ end
95
+
96
+ def test_location_and_parse_location
97
+ mock(@bucket).bucket_request(:get, {:params=>{:location=>nil}}) { @response_location }
98
+
99
+ expected = @bucket_location
100
+ actual = @bucket.location
101
+ assert_equal expected, actual
102
+
103
+ stub(@bucket).bucket_request(:get, {:params=>{:location=>nil}}) { flunk "should deliver from cached result" }
104
+ actual = @bucket.location
105
+ assert_equal expected, actual
106
+ end
107
+
108
+ def test_save
109
+ mock(@bucket).bucket_request(:put, {:headers=>{}}) { }
110
+ assert @bucket.save
111
+ # mock ensures that bucket_request was called
112
+ end
113
+
114
+ def test_save_failure_owned_by_you
115
+ mock(@bucket).bucket_request(:put, {:headers=>{}}) { raise Stree::Error::BucketAlreadyOwnedByYou.new(409, @response_owned_by_you) }
116
+ assert_raise Stree::Error::BucketAlreadyOwnedByYou do
117
+ @bucket.save
118
+ end
119
+
120
+ mock(@bucket).bucket_request(:put, {:headers=>{}}) { raise Stree::Error::BucketAlreadyExists.new(409, @response_already_exists) }
121
+ assert_raise Stree::Error::BucketAlreadyExists do
122
+ @bucket.save
123
+ end
124
+ end
125
+
126
+ def test_objects
127
+ mock(@bucket).fetch_objects { @objects_list_empty }
128
+ expected = @objects_list_empty
129
+ actual = @bucket.objects
130
+ assert_equal expected, actual
131
+
132
+ stub(@bucket).fetch_objects { flunk "should load objects from cache" }
133
+ actual = @bucket.objects
134
+ assert_equal expected, actual
135
+
136
+ stub(@bucket).fetch_objects { @objects_list }
137
+
138
+ expected = @objects_list
139
+ actual = @bucket.objects(true)
140
+ assert_equal expected, actual
141
+ end
142
+
143
+ def test_fetch_objects_and_parse_objects
144
+ mock(@bucket).bucket_request(:get, :test=>true) { @response_objects_list_empty }
145
+ expected = @objects_list_empty
146
+ actual = @bucket.objects.find_all(:test => true)
147
+ assert_equal expected, actual
148
+
149
+ mock(@bucket).bucket_request(:get, :test=>true) { @response_objects_list }
150
+ expected = @objects_list
151
+ actual = @bucket.objects.find_all(:test => true)
152
+ assert_equal expected, actual
153
+ end
154
+
155
+ def test_destroy
156
+ mock(@bucket).bucket_request(:delete) { }
157
+ assert @bucket.destroy
158
+ end
159
+
160
+ def test_objects_build
161
+ stub(@bucket).bucket_request { flunk "should not connect to server" }
162
+
163
+ expected = "object_name"
164
+ actual = @bucket.objects.build("object_name")
165
+ assert_kind_of Stree::Object, actual
166
+ assert_equal expected, actual.key
167
+ end
168
+
169
+ def test_objects_find_first
170
+ assert_nothing_raised do
171
+ stub.instance_of(Stree::Object).retrieve { Stree::Object.new(nil, "obj2") }
172
+ expected = "obj2"
173
+ actual = @bucket.objects.find_first("obj2")
174
+ assert_equal "obj2", actual.key
175
+ end
176
+ end
177
+
178
+ def test_objects_find_first_fail
179
+ assert_raise Stree::Error::NoSuchKey do
180
+ stub.instance_of(Stree::Object).retrieve { raise Stree::Error::NoSuchKey.new(404, nil) }
181
+ @bucket.objects.find_first("obj3")
182
+ end
183
+ end
184
+
185
+ def test_objects_find_all_on_empty_list
186
+ stub(@bucket).fetch_objects { @objects_list_empty }
187
+ assert_nothing_raised do
188
+ expected = @objects_list_empty
189
+ actual = @bucket.objects.find_all
190
+ assert_equal expected, actual
191
+ end
192
+ end
193
+
194
+ def test_objects_find_all
195
+ stub(@bucket).fetch_objects { @objects_list }
196
+ assert_nothing_raised do
197
+ expected = @objects_list
198
+ actual = @bucket.objects.find_all
199
+ assert_equal expected, actual
200
+ end
201
+ end
202
+
203
+ def test_objects_reload
204
+ stub(@bucket).fetch_objects { @objects_list_empty }
205
+ expected = @objects_list_empty
206
+ actual = @bucket.objects
207
+ assert_equal expected, actual
208
+
209
+ stub(@bucket).fetch_objects { @objects_list }
210
+ expected = @objects_list_empty
211
+ actual = @bucket.objects
212
+ assert_equal expected, actual
213
+
214
+ assert @bucket.objects.reload
215
+
216
+ expected = @objects_list
217
+ actual = @bucket.objects
218
+ assert_equal expected, actual
219
+ end
220
+
221
+ def test_objects_destroy_all
222
+ @counter = 0
223
+ stub(@bucket).fetch_objects { @objects_list }
224
+ @bucket.objects.each do |obj|
225
+ mock(obj).destroy { @counter += 1 }
226
+ end
227
+
228
+ @bucket.objects.destroy_all
229
+ assert_equal @objects_list.length, @counter
230
+ end
231
+ end
@@ -0,0 +1,164 @@
1
+ require 'test_helper'
2
+
3
+ class ConnectionTest < Test::Unit::TestCase
4
+ def setup
5
+ @connection = Stree::Connection.new(
6
+ :access_key_id => "12345678901234567890",
7
+ :secret_access_key => "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDF"
8
+ )
9
+ @http_request = Net::HTTP.new("")
10
+ @response_ok = Net::HTTPOK.new("1.1", "200", "OK")
11
+ @response_not_found = Net::HTTPNotFound.new("1.1", "404", "Not Found")
12
+ stub(@connection).http { @http_request }
13
+ stub(@http_request).start { @response_ok }
14
+ end
15
+
16
+ def test_handle_response_not_modify_response_when_ok
17
+ assert_nothing_raised do
18
+ response = @connection.request(
19
+ :get,
20
+ :host => "s3.amazonaws.com",
21
+ :path => "/"
22
+ )
23
+ assert_equal @response_ok, response
24
+ end
25
+ end
26
+
27
+ def test_handle_response_throws_exception_when_not_ok
28
+ response_body = <<-EOFakeBody
29
+ <?xml version=\"1.0\" encoding=\"UTF-8\"?>
30
+ <SomeResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">
31
+ <Code>NoSuchBucket</Code>
32
+ <Message>The specified bucket does not exist</Message>
33
+ </SomeResult>
34
+ EOFakeBody
35
+ stub(@http_request).start { @response_not_found }
36
+ stub(@response_not_found).body { response_body }
37
+
38
+ assert_raise Stree::Error::NoSuchBucket do
39
+ response = @connection.request(
40
+ :get,
41
+ :host => "data.example.com.s3.amazonaws.com",
42
+ :path => "/"
43
+ )
44
+ end
45
+ end
46
+
47
+ def test_handle_response_throws_standard_exception_when_not_ok
48
+ stub(@http_request).start { @response_not_found }
49
+ stub(@response_not_found).body { nil }
50
+ assert_raise Stree::Error::ResponseError do
51
+ response = @connection.request(
52
+ :get,
53
+ :host => "data.example.com.s3.amazonaws.com",
54
+ :path => "/"
55
+ )
56
+ end
57
+
58
+ stub(@response_not_found).body { "" }
59
+ assert_raise Stree::Error::ResponseError do
60
+ response = @connection.request(
61
+ :get,
62
+ :host => "data.example.com.s3.amazonaws.com",
63
+ :path => "/"
64
+ )
65
+ end
66
+ end
67
+
68
+ def test_parse_params_empty
69
+ expected = ""
70
+ actual = Stree::Connection.parse_params({})
71
+ assert_equal expected, actual
72
+ end
73
+
74
+ def test_parse_params_only_interesting_params
75
+ expected = ""
76
+ actual = Stree::Connection.parse_params(:param1 => "1", :maxkeys => "2")
77
+ assert_equal expected, actual
78
+ end
79
+
80
+ def test_parse_params_remove_underscore
81
+ expected = "max-keys=100"
82
+ actual = Stree::Connection.parse_params(:max_keys => 100)
83
+ assert_equal expected, actual
84
+ end
85
+
86
+ def test_parse_params_with_and_without_values
87
+ expected = "max-keys=100&prefix"
88
+ actual = Stree::Connection.parse_params(:max_keys => 100, :prefix => nil)
89
+ assert_equal expected, actual
90
+ end
91
+
92
+ def test_headers_headers_empty
93
+ expected = {}
94
+ actual = Stree::Connection.parse_headers({})
95
+ assert_equal expected, actual
96
+ end
97
+
98
+ def test_parse_headers_only_interesting_headers
99
+ expected = {}
100
+ actual = Stree::Connection.parse_headers(
101
+ :accept => "text/*, text/html, text/html;level=1, */*",
102
+ :accept_charset => "iso-8859-2, unicode-1-1;q=0.8"
103
+ )
104
+ assert_equal expected, actual
105
+ end
106
+
107
+ def test_parse_headers_remove_underscore
108
+ expected = {
109
+ "content-type" => nil,
110
+ "x-amz-acl" => nil,
111
+ "if-modified-since" => nil,
112
+ "if-unmodified-since" => nil,
113
+ "if-match" => nil,
114
+ "if-none-match" => nil,
115
+ "content-disposition" => nil,
116
+ "content-encoding" => nil
117
+ }
118
+ actual = Stree::Connection.parse_headers(
119
+ :content_type => nil,
120
+ :x_amz_acl => nil,
121
+ :if_modified_since => nil,
122
+ :if_unmodified_since => nil,
123
+ :if_match => nil,
124
+ :if_none_match => nil,
125
+ :content_disposition => nil,
126
+ :content_encoding => nil
127
+ )
128
+ assert_equal expected, actual
129
+ end
130
+
131
+ def test_parse_headers_with_values
132
+ expected = {
133
+ "content-type" => "text/html",
134
+ "x-amz-acl" => "public-read",
135
+ "if-modified-since" => "today",
136
+ "if-unmodified-since" => "tomorrow",
137
+ "if-match" => "1234",
138
+ "if-none-match" => "1243",
139
+ "content-disposition" => "inline",
140
+ "content-encoding" => "gzip"
141
+ }
142
+ actual = Stree::Connection.parse_headers(
143
+ :content_type => "text/html",
144
+ :x_amz_acl => "public-read",
145
+ :if_modified_since => "today",
146
+ :if_unmodified_since => "tomorrow",
147
+ :if_match => "1234",
148
+ :if_none_match => "1243",
149
+ :content_disposition => "inline",
150
+ :content_encoding => "gzip"
151
+ )
152
+ assert_equal expected, actual
153
+ end
154
+
155
+ def test_parse_headers_with_range
156
+ expected = {
157
+ "range" => "bytes=0-100"
158
+ }
159
+ actual = Stree::Connection.parse_headers(
160
+ :range => 0..100
161
+ )
162
+ assert_equal expected, actual
163
+ end
164
+ end