elasticity 5.0.1 → 5.0.2
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/HISTORY.md +5 -0
- data/lib/elasticity.rb +6 -1
- data/lib/elasticity/aws_request_v2.rb +42 -0
- data/lib/elasticity/aws_request_v4.rb +98 -0
- data/lib/elasticity/aws_session.rb +72 -0
- data/lib/elasticity/aws_utils.rb +65 -0
- data/lib/elasticity/emr.rb +1 -1
- data/lib/elasticity/version.rb +1 -1
- data/spec/lib/elasticity/aws_request_v2_spec.rb +38 -0
- data/spec/lib/elasticity/aws_request_v4_spec.rb +80 -0
- data/spec/lib/elasticity/aws_session_spec.rb +191 -0
- data/spec/lib/elasticity/aws_utils_spec.rb +105 -0
- data/spec/lib/elasticity/emr_spec.rb +26 -26
- metadata +14 -5
- data/lib/elasticity/aws_request.rb +0 -135
- data/spec/lib/elasticity/aws_request_spec.rb +0 -274
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd9e39257ac81bf6cb0a18f38d680bc0e6bdc5f1
|
4
|
+
data.tar.gz: d6f3dd0970a81a164e548e52707b29419f3ce107
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b47d9a75474f3afb40a34c9f0f7bd846fa250e0e15551b3106cd92cd804a710e8c818ac2770b65f9c57e5760d985c5a641db668094dcb9c57f0c2a694866634e
|
7
|
+
data.tar.gz: c6d00853cf474fb771bde33fdb50828ec9923d9918d059b06409fdd6ab7709184bf71db78943df23df1b30a7710504b574ccb3687b0cdf7bbf3d6c961752a8e1
|
data/HISTORY.md
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
## 5.0.2 - April 27, 2015
|
2
|
+
|
3
|
+
- Fix for issue [#83](https://github.com/rslifka/elasticity/issues/83), `elasticity` has now transitioned to the AWS [Signature Version 4 Signing Process](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html).
|
4
|
+
- Removed the ability to create insecure (HTTP) connections to the EMR endpoints.
|
5
|
+
|
1
6
|
## 5.0.1 - April 12, 2015
|
2
7
|
|
3
8
|
- Bear with me here :) Backmerged into 4.0.4 to add [IAM Service Role support](http://docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/emr-iam-roles-creatingroles.html) per @alexanderdean. As part of the forward merge, bumping the version to trigger an update.
|
data/lib/elasticity.rb
CHANGED
@@ -5,7 +5,12 @@ require 'rest_client'
|
|
5
5
|
require 'nokogiri'
|
6
6
|
require 'fog'
|
7
7
|
|
8
|
-
require 'elasticity/
|
8
|
+
require 'elasticity/version'
|
9
|
+
|
10
|
+
require 'elasticity/aws_utils'
|
11
|
+
require 'elasticity/aws_session'
|
12
|
+
require 'elasticity/aws_request_v2'
|
13
|
+
require 'elasticity/aws_request_v4'
|
9
14
|
require 'elasticity/emr'
|
10
15
|
|
11
16
|
require 'elasticity/sync_to_s3'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Elasticity
|
2
|
+
|
3
|
+
class AwsRequestV2
|
4
|
+
|
5
|
+
def initialize(aws_session, ruby_service_hash)
|
6
|
+
@aws_session = aws_session
|
7
|
+
@ruby_service_hash = ruby_service_hash
|
8
|
+
end
|
9
|
+
|
10
|
+
def url
|
11
|
+
"https://elasticmapreduce.#{@aws_session.region}.amazonaws.com"
|
12
|
+
end
|
13
|
+
|
14
|
+
def headers
|
15
|
+
{
|
16
|
+
:content_type => 'application/x-www-form-urlencoded; charset=utf-8'
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
# (Used from RightScale's right_aws gem.)
|
21
|
+
# EC2, SQS, SDB and EMR requests must be signed by this guy.
|
22
|
+
# See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
|
23
|
+
# http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1928
|
24
|
+
def payload
|
25
|
+
service_hash = AwsUtils.convert_ruby_to_aws(@ruby_service_hash)
|
26
|
+
service_hash.merge!({
|
27
|
+
'AWSAccessKeyId' => @aws_session.access_key,
|
28
|
+
'Timestamp' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
|
29
|
+
'SignatureVersion' => '2',
|
30
|
+
'SignatureMethod' => 'HmacSHA256'
|
31
|
+
})
|
32
|
+
canonical_string = service_hash.keys.sort.map do |key|
|
33
|
+
"#{AwsUtils.aws_escape(key)}=#{AwsUtils.aws_escape(service_hash[key])}"
|
34
|
+
end.join('&')
|
35
|
+
string_to_sign = "POST\n#{@aws_session.host.downcase}\n/\n#{canonical_string}"
|
36
|
+
signature = AwsUtils.aws_escape(Base64.encode64(OpenSSL::HMAC.digest('sha256', @aws_session.secret_key, string_to_sign)).strip)
|
37
|
+
"#{canonical_string}&Signature=#{signature}"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Elasticity
|
2
|
+
|
3
|
+
# To help ensure correctness, Amazon has provided a step-by-step guide of
|
4
|
+
# query-and-response conversations for various types of API calls.
|
5
|
+
#
|
6
|
+
# http://docs.aws.amazon.com/general/latest/gr/signature-v4-test-suite.html
|
7
|
+
#
|
8
|
+
# We are working with POSTs only, where the body of the POST contains the
|
9
|
+
# service details, so the 'post-x-www-form-urlencoded-parameters' suite is
|
10
|
+
# the most applicable.
|
11
|
+
class AwsRequestV4
|
12
|
+
|
13
|
+
SERVICE_NAME = 'elasticmapreduce'
|
14
|
+
|
15
|
+
def initialize(aws_session, ruby_service_hash)
|
16
|
+
@aws_session = aws_session
|
17
|
+
|
18
|
+
@ruby_service_hash = ruby_service_hash
|
19
|
+
@operation = @ruby_service_hash[:operation]
|
20
|
+
@ruby_service_hash.delete(:operation)
|
21
|
+
|
22
|
+
@timestamp = Time.now.utc
|
23
|
+
end
|
24
|
+
|
25
|
+
def headers
|
26
|
+
{
|
27
|
+
'Authorization' => "AWS4-HMAC-SHA256 Credential=#{@aws_session.access_key}/#{credential_scope}, SignedHeaders=content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target, Signature=#{aws_v4_signature}",
|
28
|
+
'Content-Type' => 'application/x-amz-json-1.1',
|
29
|
+
'Host' => host,
|
30
|
+
'User-Agent' => "elasticity/#{Elasticity::VERSION}",
|
31
|
+
'X-Amz-Content-SHA256' => Digest::SHA256.hexdigest(payload),
|
32
|
+
'X-Amz-Date' => @timestamp.strftime('%Y%m%dT%H%M%SZ'),
|
33
|
+
'X-Amz-Target' => "ElasticMapReduce.#{@operation}",
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def url
|
38
|
+
"https://#{host}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def payload
|
42
|
+
AwsUtils.convert_ruby_to_aws_v4(@ruby_service_hash).to_json
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def host
|
48
|
+
"elasticmapreduce.#{@aws_session.region}.amazonaws.com"
|
49
|
+
end
|
50
|
+
|
51
|
+
def credential_scope
|
52
|
+
"#{@timestamp.strftime('%Y%m%d')}/#{@aws_session.region}/#{SERVICE_NAME}/aws4_request"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Task 1: Create a Canonical Request For Signature Version 4
|
56
|
+
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
57
|
+
def canonical_request
|
58
|
+
[
|
59
|
+
'POST',
|
60
|
+
'/',
|
61
|
+
'',
|
62
|
+
'content-type:application/x-amz-json-1.1',
|
63
|
+
"host:#{host}",
|
64
|
+
"user-agent:elasticity/#{Elasticity::VERSION}",
|
65
|
+
"x-amz-content-sha256:#{Digest::SHA256.hexdigest(payload)}",
|
66
|
+
"x-amz-date:#{@timestamp.strftime('%Y%m%dT%H%M%SZ')}",
|
67
|
+
"x-amz-target:ElasticMapReduce.#{@operation}",
|
68
|
+
'',
|
69
|
+
'content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target',
|
70
|
+
Digest::SHA256.hexdigest(payload)
|
71
|
+
].join("\n")
|
72
|
+
end
|
73
|
+
|
74
|
+
# Task 2: Create a String to Sign for Signature Version 4
|
75
|
+
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
76
|
+
def string_to_sign
|
77
|
+
[
|
78
|
+
'AWS4-HMAC-SHA256',
|
79
|
+
@timestamp.strftime('%Y%m%dT%H%M%SZ'),
|
80
|
+
credential_scope,
|
81
|
+
Digest::SHA256.hexdigest(canonical_request)
|
82
|
+
].join("\n")
|
83
|
+
end
|
84
|
+
|
85
|
+
# Task 3: Calculate the AWS Signature Version 4
|
86
|
+
# http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
87
|
+
def aws_v4_signature
|
88
|
+
date = OpenSSL::HMAC.digest('sha256', 'AWS4' + @aws_session.secret_key, @timestamp.strftime('%Y%m%d'))
|
89
|
+
region = OpenSSL::HMAC.digest('sha256', date, @aws_session.region)
|
90
|
+
service = OpenSSL::HMAC.digest('sha256', region, SERVICE_NAME)
|
91
|
+
signing_key = OpenSSL::HMAC.digest('sha256', service, 'aws4_request')
|
92
|
+
|
93
|
+
OpenSSL::HMAC.hexdigest('sha256', signing_key, string_to_sign)
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Elasticity
|
2
|
+
|
3
|
+
class MissingKeyError < StandardError;
|
4
|
+
end
|
5
|
+
class MissingRegionError < StandardError;
|
6
|
+
end
|
7
|
+
|
8
|
+
class AwsSession
|
9
|
+
|
10
|
+
attr_reader :access_key
|
11
|
+
attr_reader :secret_key
|
12
|
+
attr_reader :host
|
13
|
+
attr_reader :region
|
14
|
+
|
15
|
+
# Supported values for options:
|
16
|
+
# :region - AWS region (e.g. us-west-1)
|
17
|
+
# :secure - true or false, default true.
|
18
|
+
def initialize(access=nil, secret=nil, options={})
|
19
|
+
# There is a cryptic error if this isn't set
|
20
|
+
if options.has_key?(:region) && options[:region] == nil
|
21
|
+
raise MissingRegionError, 'A valid :region is required to connect to EMR'
|
22
|
+
end
|
23
|
+
options[:region] = 'us-east-1' unless options[:region]
|
24
|
+
@region = options[:region]
|
25
|
+
|
26
|
+
@access_key = get_access_key(access)
|
27
|
+
@secret_key = get_secret_key(secret)
|
28
|
+
@host = "elasticmapreduce.#@region.amazonaws.com"
|
29
|
+
end
|
30
|
+
|
31
|
+
def submit(ruby_service_hash)
|
32
|
+
aws_request = AwsRequestV4.new(self, ruby_service_hash)
|
33
|
+
begin
|
34
|
+
RestClient.post(aws_request.url, aws_request.payload, aws_request.headers)
|
35
|
+
rescue RestClient::BadRequest => e
|
36
|
+
raise ArgumentError, AwsSession.parse_error_response(e.http_body)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
return false unless other.is_a? AwsSession
|
42
|
+
return false unless @access_key == other.access_key
|
43
|
+
return false unless @secret_key == other.secret_key
|
44
|
+
return false unless @host == other.host
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def get_access_key(access)
|
51
|
+
return access if access
|
52
|
+
return ENV['AWS_ACCESS_KEY_ID'] if ENV['AWS_ACCESS_KEY_ID']
|
53
|
+
raise MissingKeyError, 'Please provide an access key or set AWS_ACCESS_KEY_ID.'
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_secret_key(secret)
|
57
|
+
return secret if secret
|
58
|
+
return ENV['AWS_SECRET_ACCESS_KEY'] if ENV['AWS_SECRET_ACCESS_KEY']
|
59
|
+
raise MissingKeyError, 'Please provide a secret key or set AWS_SECRET_ACCESS_KEY.'
|
60
|
+
end
|
61
|
+
|
62
|
+
# AWS error responses all follow the same form. Extract the message from
|
63
|
+
# the error document.
|
64
|
+
def self.parse_error_response(error_xml)
|
65
|
+
xml_doc = Nokogiri::XML(error_xml)
|
66
|
+
xml_doc.remove_namespaces!
|
67
|
+
xml_doc.xpath('/ErrorResponse/Error/Message').text
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Elasticity
|
2
|
+
|
3
|
+
class AwsUtils
|
4
|
+
|
5
|
+
# Escape a string according to Amazon's rules.
|
6
|
+
# See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
|
7
|
+
def self.aws_escape(param)
|
8
|
+
param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
|
9
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# With the advent of v4 signing, we can skip the complex translation from v2
|
14
|
+
# and ship the JSON over with nearly the same structure.
|
15
|
+
def self.convert_ruby_to_aws_v4(value)
|
16
|
+
case value
|
17
|
+
when Array
|
18
|
+
return value.map{|v| convert_ruby_to_aws_v4(v)}
|
19
|
+
when Hash
|
20
|
+
result = {}
|
21
|
+
value.each do |k,v|
|
22
|
+
result[camelize(k.to_s)] = convert_ruby_to_aws_v4(v)
|
23
|
+
end
|
24
|
+
return result
|
25
|
+
else
|
26
|
+
return value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Since we use the same structure as AWS, we can generate AWS param names
|
31
|
+
# from the Ruby versions of those names (and the param nesting).
|
32
|
+
def self.convert_ruby_to_aws(params)
|
33
|
+
result = {}
|
34
|
+
params.each do |key, value|
|
35
|
+
case value
|
36
|
+
when Array
|
37
|
+
prefix = "#{camelize(key.to_s)}.member"
|
38
|
+
value.each_with_index do |item, index|
|
39
|
+
if item.is_a?(String)
|
40
|
+
result["#{prefix}.#{index+1}"] = item
|
41
|
+
else
|
42
|
+
convert_ruby_to_aws(item).each do |nested_key, nested_value|
|
43
|
+
result["#{prefix}.#{index+1}.#{nested_key}"] = nested_value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
when Hash
|
48
|
+
prefix = "#{camelize(key.to_s)}"
|
49
|
+
convert_ruby_to_aws(value).each do |nested_key, nested_value|
|
50
|
+
result["#{prefix}.#{nested_key}"] = nested_value
|
51
|
+
end
|
52
|
+
else
|
53
|
+
result[camelize(key.to_s)] = value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.camelize(word)
|
60
|
+
word.to_s.gsub(/\/(.?)/) { '::' + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/lib/elasticity/emr.rb
CHANGED
@@ -5,7 +5,7 @@ module Elasticity
|
|
5
5
|
attr_reader :aws_request
|
6
6
|
|
7
7
|
def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, options = {})
|
8
|
-
@aws_request = Elasticity::
|
8
|
+
@aws_request = Elasticity::AwsSession.new(aws_access_key_id, aws_secret_access_key, options)
|
9
9
|
end
|
10
10
|
|
11
11
|
# Describe a specific jobflow.
|
data/lib/elasticity/version.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
describe Elasticity::AwsRequestV2 do
|
2
|
+
|
3
|
+
before do
|
4
|
+
Timecop.freeze(Time.at(1302461096))
|
5
|
+
end
|
6
|
+
|
7
|
+
after do
|
8
|
+
Timecop.return
|
9
|
+
end
|
10
|
+
|
11
|
+
subject do
|
12
|
+
Elasticity::AwsRequestV2.new(
|
13
|
+
Elasticity::AwsSession.new('access', 'secret'),
|
14
|
+
{:operation => 'RunJobFlow', :name => 'Elasticity Job Flow'}
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#url' do
|
19
|
+
it 'should construct a proper endpoint' do
|
20
|
+
subject.url.should == 'https://elasticmapreduce.us-east-1.amazonaws.com'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#headers' do
|
25
|
+
it 'should create the proper headers' do
|
26
|
+
subject.headers.should == {
|
27
|
+
:content_type => 'application/x-www-form-urlencoded; charset=utf-8'
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#payload' do
|
33
|
+
it 'should payload up the place' do
|
34
|
+
subject.payload.should == 'AWSAccessKeyId=access&Name=Elasticity%20Job%20Flow&Operation=RunJobFlow&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-04-10T18%3A44%3A56.000Z&Signature=5x6YilYHOjgM%2F6nalIOf62txOKoLFGBYyIivoHb%2F27k%3D'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
describe Elasticity::AwsRequestV4 do
|
2
|
+
|
3
|
+
before do
|
4
|
+
Timecop.freeze(Time.at(1315611360))
|
5
|
+
end
|
6
|
+
|
7
|
+
after do
|
8
|
+
Timecop.return
|
9
|
+
end
|
10
|
+
|
11
|
+
subject do
|
12
|
+
Elasticity::AwsRequestV4.new(
|
13
|
+
Elasticity::AwsSession.new('access', 'secret'),
|
14
|
+
{:operation => 'DescribeJobFlows', :job_flow_ids => ['TEST_JOBFLOW_ID']}
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#url' do
|
19
|
+
it 'should construct a proper endpoint' do
|
20
|
+
subject.url.should == 'https://elasticmapreduce.us-east-1.amazonaws.com'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#headers' do
|
25
|
+
it 'should create the proper headers' do
|
26
|
+
subject.headers.should == {
|
27
|
+
'Authorization' => "AWS4-HMAC-SHA256 Credential=access/20110909/us-east-1/elasticmapreduce/aws4_request, SignedHeaders=content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target, Signature=#{subject.send(:aws_v4_signature)}",
|
28
|
+
'Content-Type' => 'application/x-amz-json-1.1',
|
29
|
+
'Host' => 'elasticmapreduce.us-east-1.amazonaws.com',
|
30
|
+
'User-Agent' => "elasticity/#{Elasticity::VERSION}",
|
31
|
+
'X-Amz-Content-SHA256' => Digest::SHA256.hexdigest(subject.payload),
|
32
|
+
'X-Amz-Date' => '20110909T233600Z',
|
33
|
+
'X-Amz-Target' => 'ElasticMapReduce.DescribeJobFlows',
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#payload' do
|
39
|
+
it 'should create the proper payload' do
|
40
|
+
subject.payload.should == '{"JobFlowIds":["TEST_JOBFLOW_ID"]}'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '.canonical_request' do
|
45
|
+
it 'should create the proper canonical request' do
|
46
|
+
subject.send(:canonical_request).should == [
|
47
|
+
'POST',
|
48
|
+
'/',
|
49
|
+
'',
|
50
|
+
'content-type:application/x-amz-json-1.1',
|
51
|
+
'host:elasticmapreduce.us-east-1.amazonaws.com',
|
52
|
+
"user-agent:elasticity/#{Elasticity::VERSION}",
|
53
|
+
"x-amz-content-sha256:#{Digest::SHA256.hexdigest(subject.payload)}",
|
54
|
+
'x-amz-date:20110909T233600Z',
|
55
|
+
'x-amz-target:ElasticMapReduce.DescribeJobFlows',
|
56
|
+
'',
|
57
|
+
'content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target',
|
58
|
+
"#{Digest::SHA256.hexdigest(subject.payload)}"
|
59
|
+
].join("\n")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '.string_to_sign' do
|
64
|
+
it 'should create the proper string to sign' do
|
65
|
+
subject.send(:string_to_sign).should == [
|
66
|
+
'AWS4-HMAC-SHA256',
|
67
|
+
'20110909T233600Z',
|
68
|
+
'20110909/us-east-1/elasticmapreduce/aws4_request',
|
69
|
+
"#{Digest::SHA256.hexdigest(subject.send(:canonical_request))}"
|
70
|
+
].join("\n")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '.aws_v4_signature' do
|
75
|
+
it 'should create the proper signature' do
|
76
|
+
subject.send(:aws_v4_signature).should == '3e88b95410e6828f80b4ec476bcf7e23ab8dd380b22ffcb1d5f7e86390346f68'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|