dbourguignon-aws-s3 0.6.3
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.
- data/COPYING +19 -0
- data/INSTALL +55 -0
- data/README +545 -0
- data/Rakefile +334 -0
- data/bin/s3sh +6 -0
- data/bin/setup.rb +10 -0
- data/lib/aws/s3.rb +60 -0
- data/lib/aws/s3/acl.rb +636 -0
- data/lib/aws/s3/authentication.rb +221 -0
- data/lib/aws/s3/base.rb +240 -0
- data/lib/aws/s3/bittorrent.rb +58 -0
- data/lib/aws/s3/bucket.rb +319 -0
- data/lib/aws/s3/connection.rb +278 -0
- data/lib/aws/s3/error.rb +69 -0
- data/lib/aws/s3/exceptions.rb +133 -0
- data/lib/aws/s3/extensions.rb +356 -0
- data/lib/aws/s3/logging.rb +314 -0
- data/lib/aws/s3/object.rb +615 -0
- data/lib/aws/s3/owner.rb +44 -0
- data/lib/aws/s3/parsing.rb +99 -0
- data/lib/aws/s3/response.rb +180 -0
- data/lib/aws/s3/service.rb +51 -0
- data/lib/aws/s3/version.rb +12 -0
- data/test/acl_test.rb +254 -0
- data/test/authentication_test.rb +114 -0
- data/test/base_test.rb +136 -0
- data/test/bucket_test.rb +74 -0
- data/test/connection_test.rb +215 -0
- data/test/error_test.rb +70 -0
- data/test/extensions_test.rb +340 -0
- data/test/fixtures.rb +89 -0
- data/test/logging_test.rb +89 -0
- data/test/object_test.rb +205 -0
- data/test/parsing_test.rb +66 -0
- data/test/response_test.rb +68 -0
- data/test/service_test.rb +23 -0
- data/test/test_helper.rb +110 -0
- metadata +125 -0
data/lib/aws/s3/owner.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module AWS
|
2
|
+
module S3
|
3
|
+
# Entities in S3 have an associated owner (the person who created them). The owner is a canonical representation of an
|
4
|
+
# entity in the S3 system. It has an <tt>id</tt> and a <tt>display_name</tt>.
|
5
|
+
#
|
6
|
+
# These attributes can be used when specifying a ACL::Grantee for an ACL::Grant.
|
7
|
+
#
|
8
|
+
# You can retrieve the owner of the current account by calling Owner.current.
|
9
|
+
class Owner
|
10
|
+
undef_method :id if method_defined?(:id) # Get rid of Object#id
|
11
|
+
include SelectiveAttributeProxy
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# The owner of the current account.
|
15
|
+
def current
|
16
|
+
response = Service.get('/')
|
17
|
+
new(response.parsed['owner']) if response.parsed['owner']
|
18
|
+
end
|
19
|
+
memoized :current
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(attributes = {}) #:nodoc:
|
23
|
+
@attributes = attributes
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other_owner) #:nodoc:
|
27
|
+
hash == other_owner.hash
|
28
|
+
end
|
29
|
+
|
30
|
+
def hash #:nodoc
|
31
|
+
[id, display_name].join.hash
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def proxiable_attribute?(name)
|
36
|
+
valid_attributes.include?(name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def valid_attributes
|
40
|
+
%w(id display_name)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
#:stopdoc:
|
2
|
+
module AWS
|
3
|
+
module S3
|
4
|
+
module Parsing
|
5
|
+
class << self
|
6
|
+
def parser=(parsing_library)
|
7
|
+
XmlParser.parsing_library = parsing_library
|
8
|
+
end
|
9
|
+
|
10
|
+
def parser
|
11
|
+
XmlParser.parsing_library
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module Typecasting
|
16
|
+
def typecast(object)
|
17
|
+
case object
|
18
|
+
when Hash
|
19
|
+
typecast_hash(object)
|
20
|
+
when Array
|
21
|
+
object.map {|element| typecast(element)}
|
22
|
+
when String
|
23
|
+
CoercibleString.coerce(object)
|
24
|
+
else
|
25
|
+
object
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def typecast_hash(hash)
|
30
|
+
if content = hash['__content__']
|
31
|
+
typecast(content)
|
32
|
+
else
|
33
|
+
keys = hash.keys.map {|key| key.underscore}
|
34
|
+
values = hash.values.map {|value| typecast(value)}
|
35
|
+
keys.inject({}) do |new_hash, key|
|
36
|
+
new_hash[key] = values.slice!(0)
|
37
|
+
new_hash
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class XmlParser < Hash
|
44
|
+
include Typecasting
|
45
|
+
|
46
|
+
class << self
|
47
|
+
attr_accessor :parsing_library
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_reader :body, :xml_in, :root
|
51
|
+
|
52
|
+
def initialize(body)
|
53
|
+
@body = body
|
54
|
+
unless body.strip.empty?
|
55
|
+
parse
|
56
|
+
set_root
|
57
|
+
typecast_xml_in
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def parse
|
64
|
+
@xml_in = self.class.parsing_library.xml_in(body, parsing_options)
|
65
|
+
end
|
66
|
+
|
67
|
+
def parsing_options
|
68
|
+
{
|
69
|
+
# Includes the enclosing tag as the top level key
|
70
|
+
'keeproot' => true,
|
71
|
+
# Makes tag value available via the '__content__' key
|
72
|
+
'contentkey' => '__content__',
|
73
|
+
# Always parse tags into a hash, even when there are no attributes
|
74
|
+
# (unless there is also no value, in which case it is nil)
|
75
|
+
'forcecontent' => true,
|
76
|
+
# If a tag is empty, makes its content nil
|
77
|
+
'suppressempty' => nil,
|
78
|
+
# Force nested elements to be put into an array, even if there is only one of them
|
79
|
+
'forcearray' => ['Contents', 'Bucket', 'Grant']
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_root
|
84
|
+
@root = @xml_in.keys.first.underscore
|
85
|
+
end
|
86
|
+
|
87
|
+
def typecast_xml_in
|
88
|
+
typecast_xml = {}
|
89
|
+
@xml_in.dup.each do |key, value| # Some typecasting is destructive so we dup
|
90
|
+
typecast_xml[key.underscore] = typecast(value)
|
91
|
+
end
|
92
|
+
# An empty body will try to update with a string so only update if the result is a hash
|
93
|
+
update(typecast_xml[root]) if typecast_xml[root].is_a?(Hash)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
#:startdoc:
|
@@ -0,0 +1,180 @@
|
|
1
|
+
#:stopdoc:
|
2
|
+
module AWS
|
3
|
+
module S3
|
4
|
+
class Base
|
5
|
+
class Response < String
|
6
|
+
attr_reader :response, :body, :parsed
|
7
|
+
def initialize(response)
|
8
|
+
@response = response
|
9
|
+
@body = response.body.to_s
|
10
|
+
super(body)
|
11
|
+
end
|
12
|
+
|
13
|
+
def headers
|
14
|
+
headers = {}
|
15
|
+
response.each do |header, value|
|
16
|
+
headers[header] = value
|
17
|
+
end
|
18
|
+
headers
|
19
|
+
end
|
20
|
+
memoized :headers
|
21
|
+
|
22
|
+
def [](header)
|
23
|
+
headers[header]
|
24
|
+
end
|
25
|
+
|
26
|
+
def each(&block)
|
27
|
+
headers.each(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def code
|
31
|
+
response.code.to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
{:success => 200..299, :redirect => 300..399,
|
35
|
+
:client_error => 400..499, :server_error => 500..599}.each do |result, code_range|
|
36
|
+
class_eval(<<-EVAL, __FILE__, __LINE__)
|
37
|
+
def #{result}?
|
38
|
+
return false unless response
|
39
|
+
(#{code_range}).include? code
|
40
|
+
end
|
41
|
+
EVAL
|
42
|
+
end
|
43
|
+
|
44
|
+
def error?
|
45
|
+
!success? && response['content-type'] == 'application/xml' && parsed.root == 'error'
|
46
|
+
end
|
47
|
+
|
48
|
+
def error
|
49
|
+
Error.new(parsed, self)
|
50
|
+
end
|
51
|
+
memoized :error
|
52
|
+
|
53
|
+
def parsed
|
54
|
+
# XmlSimple is picky about what kind of object it parses, so we pass in body rather than self
|
55
|
+
Parsing::XmlParser.new(body)
|
56
|
+
end
|
57
|
+
memoized :parsed
|
58
|
+
|
59
|
+
def inspect
|
60
|
+
"#<%s:0x%s %s %s>" % [self.class, object_id, response.code, response.message]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class Bucket
|
66
|
+
class Response < Base::Response
|
67
|
+
def bucket
|
68
|
+
parsed
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class S3Object
|
74
|
+
class Response < Base::Response
|
75
|
+
def etag
|
76
|
+
headers['etag'][1...-1]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Service
|
82
|
+
class Response < Base::Response
|
83
|
+
def empty?
|
84
|
+
parsed['buckets'].nil?
|
85
|
+
end
|
86
|
+
|
87
|
+
def buckets
|
88
|
+
parsed['buckets']['bucket'] || []
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
module ACL
|
94
|
+
class Policy
|
95
|
+
class Response < Base::Response
|
96
|
+
alias_method :policy, :parsed
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Requests whose response code is between 300 and 599 and contain an <Error></Error> in their body
|
102
|
+
# are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception
|
103
|
+
# that corresponds to the error in the response body. The exception object contains the ErrorResponse, so
|
104
|
+
# in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and
|
105
|
+
# its Error object which contains information about the ResponseError.
|
106
|
+
#
|
107
|
+
# begin
|
108
|
+
# Bucket.create(..)
|
109
|
+
# rescue ResponseError => exception
|
110
|
+
# exception.response
|
111
|
+
# # => <Error::Response>
|
112
|
+
# exception.response.error
|
113
|
+
# # => <Error>
|
114
|
+
# end
|
115
|
+
class Error
|
116
|
+
class Response < Base::Response
|
117
|
+
def error?
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
"#<%s:0x%s %s %s: '%s'>" % [self.class.name, object_id, response.code, error.code, error.message]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Guess response class name from current class name. If the guessed response class doesn't exist
|
128
|
+
# do the same thing to the current class's parent class, up the inheritance heirarchy until either
|
129
|
+
# a response class is found or until we get to the top of the heirarchy in which case we just use
|
130
|
+
# the the Base response class.
|
131
|
+
#
|
132
|
+
# Important: This implemantation assumes that the Base class has a corresponding Base::Response.
|
133
|
+
class FindResponseClass #:nodoc:
|
134
|
+
class << self
|
135
|
+
def for(start)
|
136
|
+
new(start).find
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def initialize(start)
|
141
|
+
@container = AWS::S3
|
142
|
+
@current_class = start
|
143
|
+
end
|
144
|
+
|
145
|
+
def find
|
146
|
+
self.current_class = current_class.superclass until response_class_found?
|
147
|
+
target.const_get(class_to_find)
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
attr_reader :container
|
152
|
+
attr_accessor :current_class
|
153
|
+
|
154
|
+
def target
|
155
|
+
container.const_get(current_name)
|
156
|
+
end
|
157
|
+
|
158
|
+
def target?
|
159
|
+
container.const_defined?(current_name)
|
160
|
+
end
|
161
|
+
|
162
|
+
def response_class_found?
|
163
|
+
target? && target.const_defined?(class_to_find)
|
164
|
+
end
|
165
|
+
|
166
|
+
def class_to_find
|
167
|
+
:Response
|
168
|
+
end
|
169
|
+
|
170
|
+
def current_name
|
171
|
+
truncate(current_class)
|
172
|
+
end
|
173
|
+
|
174
|
+
def truncate(klass)
|
175
|
+
klass.name[/[^:]+$/]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
#:startdoc:
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module AWS
|
2
|
+
module S3
|
3
|
+
# The service lets you find out general information about your account, like what buckets you have.
|
4
|
+
#
|
5
|
+
# Service.buckets
|
6
|
+
# # => []
|
7
|
+
class Service < Base
|
8
|
+
@@response = nil #:nodoc:
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# List all your buckets.
|
12
|
+
#
|
13
|
+
# Service.buckets
|
14
|
+
# # => []
|
15
|
+
#
|
16
|
+
# For performance reasons, the bucket list will be cached. If you want avoid all caching, pass the <tt>:reload</tt>
|
17
|
+
# as an argument:
|
18
|
+
#
|
19
|
+
# Service.buckets(:reload)
|
20
|
+
def buckets
|
21
|
+
response = get('/')
|
22
|
+
if response.empty?
|
23
|
+
[]
|
24
|
+
else
|
25
|
+
response.buckets.map {|attributes| Bucket.new(attributes)}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
memoized :buckets
|
29
|
+
|
30
|
+
# Sometimes methods that make requests to the S3 servers return some object, like a Bucket or an S3Object.
|
31
|
+
# Othertimes they return just <tt>true</tt>. Other times they raise an exception that you may want to rescue. Despite all these
|
32
|
+
# possible outcomes, every method that makes a request stores its response object for you in Service.response. You can always
|
33
|
+
# get to the last request's response via Service.response.
|
34
|
+
#
|
35
|
+
# objects = Bucket.objects('jukebox')
|
36
|
+
# Service.response.success?
|
37
|
+
# # => true
|
38
|
+
#
|
39
|
+
# This is also useful when an error exception is raised in the console which you weren't expecting. You can
|
40
|
+
# root around in the response to get more details of what might have gone wrong.
|
41
|
+
def response
|
42
|
+
@@response
|
43
|
+
end
|
44
|
+
|
45
|
+
def response=(response) #:nodoc:
|
46
|
+
@@response = response
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/test/acl_test.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class PolicyReadingTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@policy = prepare_policy
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_policy_owner
|
10
|
+
assert_kind_of Owner, @policy.owner
|
11
|
+
assert_equal 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1', @policy.owner.id
|
12
|
+
assert_equal 'mmolina@onramp.net', @policy.owner.display_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_grants
|
16
|
+
assert @policy.grants
|
17
|
+
assert !@policy.grants.empty?
|
18
|
+
grant = @policy.grants.first
|
19
|
+
assert_kind_of ACL::Grant, grant
|
20
|
+
assert_equal 'FULL_CONTROL', grant.permission
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_grants_have_grantee
|
24
|
+
grant = @policy.grants.first
|
25
|
+
assert grantee = grant.grantee
|
26
|
+
assert_kind_of ACL::Grantee, grantee
|
27
|
+
assert_equal 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1', grantee.id
|
28
|
+
assert_equal 'mmolina@onramp.net', grantee.display_name
|
29
|
+
assert_equal 'CanonicalUser', grantee.type
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_grantee_always_responds_to_email_address
|
33
|
+
assert_nothing_raised do
|
34
|
+
@policy.grants.first.grantee.email_address
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def prepare_policy
|
40
|
+
ACL::Policy.new(parsed_policy)
|
41
|
+
end
|
42
|
+
|
43
|
+
def parsed_policy
|
44
|
+
Parsing::XmlParser.new Fixtures::Policies.policy_with_one_grant
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class PolicyWritingTest < PolicyReadingTest
|
49
|
+
|
50
|
+
def setup
|
51
|
+
policy = prepare_policy
|
52
|
+
# Dump the policy to xml and retranslate it back from the xml then run all the tests in the xml reading
|
53
|
+
# test. This round tripping indirectly asserts that the original xml document is the same as the to_xml
|
54
|
+
# dump.
|
55
|
+
@policy = ACL::Policy.new(Parsing::XmlParser.new(policy.to_xml))
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
class PolicyTest < Test::Unit::TestCase
|
61
|
+
def test_building_policy_by_hand
|
62
|
+
policy = grant = grantee = nil
|
63
|
+
assert_nothing_raised do
|
64
|
+
policy = ACL::Policy.new
|
65
|
+
grant = ACL::Grant.new
|
66
|
+
grantee = ACL::Grantee.new
|
67
|
+
grantee.email_address = 'marcel@vernix.org'
|
68
|
+
grant.permission = 'READ_ACP'
|
69
|
+
grant.grantee = grantee
|
70
|
+
policy.grants << grant
|
71
|
+
policy.owner = Owner.new('id' => '123456789', 'display_name' => 'noradio')
|
72
|
+
end
|
73
|
+
|
74
|
+
assert_nothing_raised do
|
75
|
+
policy.to_xml
|
76
|
+
end
|
77
|
+
|
78
|
+
assert !policy.grants.empty?
|
79
|
+
assert_equal 1, policy.grants.size
|
80
|
+
assert_equal 'READ_ACP', policy.grants.first.permission
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_include?
|
84
|
+
policy = ACL::Policy.new(Parsing::XmlParser.new(Fixtures::Policies.policy_with_one_grant))
|
85
|
+
assert !policy.grants.include?(:public_read)
|
86
|
+
policy.grants << ACL::Grant.grant(:public_read)
|
87
|
+
assert policy.grants.include?(:public_read)
|
88
|
+
|
89
|
+
assert policy.grants.include?(ACL::Grant.grant(:public_read))
|
90
|
+
[false, 1, '1'].each do |non_grant|
|
91
|
+
assert !policy.grants.include?(non_grant)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_delete
|
96
|
+
policy = ACL::Policy.new(Parsing::XmlParser.new(Fixtures::Policies.policy_with_one_grant))
|
97
|
+
policy.grants << ACL::Grant.grant(:public_read)
|
98
|
+
assert policy.grants.include?(:public_read)
|
99
|
+
assert policy.grants.delete(:public_read)
|
100
|
+
assert !policy.grants.include?(:public_read)
|
101
|
+
[false, 1, '1'].each do |non_grant|
|
102
|
+
assert_nil policy.grants.delete(non_grant)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_grant_list_comparison
|
107
|
+
policy = ACL::Policy.new
|
108
|
+
policy2 = ACL::Policy.new
|
109
|
+
|
110
|
+
grant_names = [:public_read, :public_read_acp, :authenticated_write]
|
111
|
+
grant_names.each {|grant_name| policy.grants << ACL::Grant.grant(grant_name)}
|
112
|
+
grant_names.reverse_each {|grant_name| policy2.grants << ACL::Grant.grant(grant_name)}
|
113
|
+
|
114
|
+
assert_equal policy.grants, policy2.grants
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class GrantTest < Test::Unit::TestCase
|
119
|
+
def test_permission_must_be_valid
|
120
|
+
grant = ACL::Grant.new
|
121
|
+
assert_nothing_raised do
|
122
|
+
grant.permission = 'READ_ACP'
|
123
|
+
end
|
124
|
+
|
125
|
+
assert_raises(InvalidAccessControlLevel) do
|
126
|
+
grant.permission = 'not a valid permission'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_stock_grants
|
131
|
+
assert_raises(ArgumentError) do
|
132
|
+
ACL::Grant.grant :this_is_not_a_stock_grant
|
133
|
+
end
|
134
|
+
|
135
|
+
grant = nil
|
136
|
+
assert_nothing_raised do
|
137
|
+
grant = ACL::Grant.grant(:public_read)
|
138
|
+
end
|
139
|
+
|
140
|
+
assert grant
|
141
|
+
assert_kind_of ACL::Grant, grant
|
142
|
+
assert_equal 'READ', grant.permission
|
143
|
+
assert grant.grantee
|
144
|
+
assert_kind_of ACL::Grantee, grant.grantee
|
145
|
+
assert_equal 'AllUsers', grant.grantee.group
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class GranteeTest < Test::Unit::TestCase
|
150
|
+
def test_type_inference
|
151
|
+
grantee = ACL::Grantee.new
|
152
|
+
|
153
|
+
assert_nothing_raised do
|
154
|
+
grantee.type
|
155
|
+
end
|
156
|
+
|
157
|
+
assert_nil grantee.type
|
158
|
+
grantee.group = 'AllUsers'
|
159
|
+
assert_equal 'AllUsers', grantee.group
|
160
|
+
assert_equal 'Group', grantee.type
|
161
|
+
grantee.email_address = 'marcel@vernix.org'
|
162
|
+
assert_equal 'AmazonCustomerByEmail', grantee.type
|
163
|
+
grantee.display_name = 'noradio'
|
164
|
+
assert_equal 'AmazonCustomerByEmail', grantee.type
|
165
|
+
grantee.id = '123456789'
|
166
|
+
assert_equal 'CanonicalUser', grantee.type
|
167
|
+
end
|
168
|
+
|
169
|
+
def test_type_is_extracted_if_present
|
170
|
+
grantee = ACL::Grantee.new('xsi:type' => 'CanonicalUser')
|
171
|
+
assert_equal 'CanonicalUser', grantee.type
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_type_representation
|
175
|
+
grantee = ACL::Grantee.new('uri' => 'http://acs.amazonaws.com/groups/global/AllUsers')
|
176
|
+
|
177
|
+
assert_equal 'AllUsers Group', grantee.type_representation
|
178
|
+
grantee.group = 'AuthenticatedUsers'
|
179
|
+
assert_equal 'AuthenticatedUsers Group', grantee.type_representation
|
180
|
+
grantee.email_address = 'marcel@vernix.org'
|
181
|
+
assert_equal 'marcel@vernix.org', grantee.type_representation
|
182
|
+
grantee.display_name = 'noradio'
|
183
|
+
grantee.id = '123456789'
|
184
|
+
assert_equal 'noradio', grantee.type_representation
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
class ACLOptionProcessorTest < Test::Unit::TestCase
|
189
|
+
def test_empty_options
|
190
|
+
options = {}
|
191
|
+
assert_nothing_raised do
|
192
|
+
process! options
|
193
|
+
end
|
194
|
+
assert_equal({}, options)
|
195
|
+
end
|
196
|
+
|
197
|
+
def test_invalid_access_level
|
198
|
+
options = {:access => :foo}
|
199
|
+
assert_raises(InvalidAccessControlLevel) do
|
200
|
+
process! options
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_valid_access_level_is_normalized
|
205
|
+
valid_access_levels = [
|
206
|
+
{:access => :private},
|
207
|
+
{'access' => 'private'},
|
208
|
+
{:access => 'private'},
|
209
|
+
{'access' => :private},
|
210
|
+
{'x-amz-acl' => 'private'},
|
211
|
+
{:x_amz_acl => :private},
|
212
|
+
{:x_amz_acl => 'private'},
|
213
|
+
{'x_amz_acl' => :private}
|
214
|
+
]
|
215
|
+
|
216
|
+
valid_access_levels.each do |options|
|
217
|
+
assert_nothing_raised do
|
218
|
+
process! options
|
219
|
+
end
|
220
|
+
assert_equal 'private', acl(options)
|
221
|
+
end
|
222
|
+
|
223
|
+
valid_hyphenated_access_levels = [
|
224
|
+
{:access => :public_read},
|
225
|
+
{'access' => 'public_read'},
|
226
|
+
{'access' => 'public-read'},
|
227
|
+
{:access => 'public_read'},
|
228
|
+
{:access => 'public-read'},
|
229
|
+
{'access' => :public_read},
|
230
|
+
|
231
|
+
{'x-amz-acl' => 'public_read'},
|
232
|
+
{:x_amz_acl => :public_read},
|
233
|
+
{:x_amz_acl => 'public_read'},
|
234
|
+
{:x_amz_acl => 'public-read'},
|
235
|
+
{'x_amz_acl' => :public_read}
|
236
|
+
]
|
237
|
+
|
238
|
+
valid_hyphenated_access_levels.each do |options|
|
239
|
+
assert_nothing_raised do
|
240
|
+
process! options
|
241
|
+
end
|
242
|
+
assert_equal 'public-read', acl(options)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
def process!(options)
|
248
|
+
ACL::OptionProcessor.process!(options)
|
249
|
+
end
|
250
|
+
|
251
|
+
def acl(options)
|
252
|
+
options['x-amz-acl']
|
253
|
+
end
|
254
|
+
end
|