jeff 0.4.3 → 0.5.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.
- data/README.md +0 -8
- data/jeff.gemspec +1 -1
- data/lib/jeff.rb +175 -2
- data/lib/jeff/secret.rb +1 -1
- data/lib/jeff/version.rb +1 -1
- data/spec/jeff/secret_spec.rb +1 -0
- data/spec/jeff_spec.rb +167 -0
- data/spec/spec_helper.rb +0 -6
- metadata +23 -25
- data/lib/jeff/serviceable.rb +0 -184
- data/spec/jeff/serviceable_spec.rb +0 -169
data/README.md
CHANGED
data/jeff.gemspec
CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.require_paths = ['lib']
|
17
17
|
gem.version = Jeff::VERSION
|
18
18
|
|
19
|
-
gem.add_dependency 'excon', '~> 0.
|
19
|
+
gem.add_dependency 'excon', '~> 0.18.0'
|
20
20
|
gem.add_development_dependency 'rake'
|
21
21
|
gem.add_development_dependency 'rspec'
|
22
22
|
|
data/lib/jeff.rb
CHANGED
@@ -1,7 +1,180 @@
|
|
1
|
-
require '
|
1
|
+
require 'base64'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'excon'
|
4
|
+
require 'time'
|
2
5
|
|
6
|
+
require 'jeff/secret'
|
7
|
+
require 'jeff/version'
|
8
|
+
|
9
|
+
# Mixes in Amazon Web Services (AWS) client behaviour.
|
3
10
|
module Jeff
|
11
|
+
MissingEndpoint = Class.new ArgumentError
|
12
|
+
MissingKey = Class.new ArgumentError
|
13
|
+
MissingSecret = Class.new ArgumentError
|
14
|
+
|
15
|
+
UNRESERVED = /([^\w.~-]+)/
|
16
|
+
|
17
|
+
# A User-Agent header that identifies the application, its version number,
|
18
|
+
# and programming language.
|
19
|
+
#
|
20
|
+
# Amazon recommends to include one in requests to AWS endpoints.
|
21
|
+
USER_AGENT = "Jeff/#{VERSION} (Language=Ruby; #{`hostname`.chomp})"
|
22
|
+
|
4
23
|
def self.included(base)
|
5
|
-
base.
|
24
|
+
base.extend ClassMethods
|
25
|
+
|
26
|
+
base.headers 'User-Agent' => USER_AGENT
|
27
|
+
|
28
|
+
base.params 'AWSAccessKeyId' => -> { key },
|
29
|
+
'SignatureVersion' => '2',
|
30
|
+
'SignatureMethod' => 'HmacSHA256',
|
31
|
+
'Timestamp' => -> { Time.now.utc.iso8601 }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Internal: Builds a sorted query.
|
35
|
+
#
|
36
|
+
# hsh - A hash of query parameters specific to the request.
|
37
|
+
#
|
38
|
+
# Returns a query String.
|
39
|
+
def build_query(hsh)
|
40
|
+
params
|
41
|
+
.merge(hsh)
|
42
|
+
.sort
|
43
|
+
.map { |k, v| "#{k}=#{ escape v }" }
|
44
|
+
.join('&')
|
45
|
+
end
|
46
|
+
|
47
|
+
# Internal: Returns an Excon::Connection.
|
48
|
+
def connection
|
49
|
+
@connection ||= Excon.new(endpoint, headers: headers, expects: 200)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Internal: Gets the String AWS endpoint.
|
53
|
+
#
|
54
|
+
# Raises a MissingEndpoint error if endpoint is missing.
|
55
|
+
def endpoint
|
56
|
+
@endpoint or raise MissingEndpoint
|
57
|
+
end
|
58
|
+
|
59
|
+
# Sets the String AWS endpoint.
|
60
|
+
attr_writer :endpoint
|
61
|
+
|
62
|
+
# Internal: Returns the Hash default headers.
|
63
|
+
def headers
|
64
|
+
self.class.headers
|
65
|
+
end
|
66
|
+
|
67
|
+
# Internal: Gets the String AWS access key id.
|
68
|
+
#
|
69
|
+
# Raises a MissingKey error if key is missing.
|
70
|
+
def key
|
71
|
+
@key or raise MissingKey
|
72
|
+
end
|
73
|
+
|
74
|
+
# Sets the String AWS access key id.
|
75
|
+
attr_writer :key
|
76
|
+
|
77
|
+
# Internal: Returns the Hash default request parameters.
|
78
|
+
def params
|
79
|
+
self.class.params.reduce({}) do |a, (k, v)|
|
80
|
+
a.update k => (v.respond_to?(:call) ? instance_exec(&v) : v)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Internal: Gets the Jeff::Secret.
|
85
|
+
#
|
86
|
+
# Raises a MissingSecret error if secret is missing.
|
87
|
+
def secret
|
88
|
+
@secret or raise MissingSecret
|
89
|
+
end
|
90
|
+
|
91
|
+
# Sets the AWS secret key.
|
92
|
+
#
|
93
|
+
# key - A String secret.
|
94
|
+
#
|
95
|
+
# Returns a Jeff::Secret.
|
96
|
+
def secret=(key)
|
97
|
+
@secret = Secret.new(key)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Generate HTTP request verb methods.
|
101
|
+
Excon::HTTP_VERBS.each do |method|
|
102
|
+
eval <<-DEF
|
103
|
+
def #{method}(opts = {})
|
104
|
+
opts.update :method => :#{method}
|
105
|
+
if opts[:body]
|
106
|
+
opts[:headers] ||= {}
|
107
|
+
opts[:headers].update 'Content-MD5' => calculate_md5(opts[:body])
|
108
|
+
end
|
109
|
+
|
110
|
+
connection.request(sign(opts))
|
111
|
+
end
|
112
|
+
DEF
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def calculate_md5(body)
|
118
|
+
Base64.encode64(Digest::MD5.digest(body)).strip
|
119
|
+
end
|
120
|
+
|
121
|
+
def connection_host
|
122
|
+
[
|
123
|
+
connection.data[:host],
|
124
|
+
connection.data[:port]
|
125
|
+
].join ':'
|
126
|
+
end
|
127
|
+
|
128
|
+
def connection_path
|
129
|
+
connection.data[:path]
|
130
|
+
end
|
131
|
+
|
132
|
+
def escape(val)
|
133
|
+
val.to_s.gsub(UNRESERVED) do
|
134
|
+
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def sign(opts)
|
139
|
+
query = build_query(opts[:query] || {})
|
140
|
+
|
141
|
+
string_to_sign = [
|
142
|
+
opts[:method].upcase,
|
143
|
+
connection_host,
|
144
|
+
opts[:path] || connection_path,
|
145
|
+
query
|
146
|
+
].join "\n"
|
147
|
+
signature = secret.sign(string_to_sign)
|
148
|
+
|
149
|
+
opts.update query: [
|
150
|
+
query,
|
151
|
+
"Signature=#{escape(signature)}"
|
152
|
+
].join('&')
|
153
|
+
end
|
154
|
+
|
155
|
+
module ClassMethods
|
156
|
+
# Gets/Updates the default headers.
|
157
|
+
#
|
158
|
+
# hsh - A Hash of headers.
|
159
|
+
#
|
160
|
+
# Returns the Hash headers.
|
161
|
+
def headers(hsh = nil)
|
162
|
+
@headers ||= {}
|
163
|
+
@headers.update(hsh) if hsh
|
164
|
+
|
165
|
+
@headers
|
166
|
+
end
|
167
|
+
|
168
|
+
# Gets/Updates the default request parameters.
|
169
|
+
#
|
170
|
+
# hsh - A Hash of parameters (default: nil).
|
171
|
+
#
|
172
|
+
# Returns the Hash parameters.
|
173
|
+
def params(hsh = nil)
|
174
|
+
@params ||= {}
|
175
|
+
@params.update(hsh) if hsh
|
176
|
+
|
177
|
+
@params
|
178
|
+
end
|
6
179
|
end
|
7
180
|
end
|
data/lib/jeff/secret.rb
CHANGED
data/lib/jeff/version.rb
CHANGED
data/spec/jeff/secret_spec.rb
CHANGED
data/spec/jeff_spec.rb
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Jeff do
|
4
|
+
let(:klass) { Class.new { include Jeff } }
|
5
|
+
let(:client) { klass.new }
|
6
|
+
|
7
|
+
describe '.headers' do
|
8
|
+
subject { klass.headers }
|
9
|
+
|
10
|
+
it { should have_key 'User-Agent' }
|
11
|
+
|
12
|
+
it 'should be configurable' do
|
13
|
+
klass.instance_eval do
|
14
|
+
headers 'Foo' => 'bar'
|
15
|
+
end
|
16
|
+
|
17
|
+
should have_key 'Foo'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '.params' do
|
22
|
+
subject { klass.params }
|
23
|
+
|
24
|
+
it { should have_key 'AWSAccessKeyId' }
|
25
|
+
|
26
|
+
it { should have_key 'SignatureMethod' }
|
27
|
+
|
28
|
+
it { should have_key 'SignatureVersion' }
|
29
|
+
|
30
|
+
it { should have_key 'Timestamp' }
|
31
|
+
|
32
|
+
it 'should be configurable' do
|
33
|
+
klass.instance_eval do
|
34
|
+
params 'Foo' => 'bar'
|
35
|
+
end
|
36
|
+
|
37
|
+
should have_key 'Foo'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#endpoint' do
|
42
|
+
it 'should require a value' do
|
43
|
+
expect { client.endpoint }.to raise_error Jeff::MissingEndpoint
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#key' do
|
48
|
+
it 'should require a value' do
|
49
|
+
expect { client.key }.to raise_error Jeff::MissingKey
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#secret' do
|
54
|
+
it 'should require a value' do
|
55
|
+
expect { client.secret }.to raise_error Jeff::MissingSecret
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'given a key' do
|
60
|
+
before do
|
61
|
+
client.key = 'key'
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#params' do
|
65
|
+
subject { client.params }
|
66
|
+
|
67
|
+
it 'should include the key' do
|
68
|
+
subject['AWSAccessKeyId'].should eql client.key
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should generate a timestamp' do
|
72
|
+
subject['Timestamp'].should be_a String
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#build_query' do
|
77
|
+
subject { client.build_query 'A10' => 1, 'A1' => 1 }
|
78
|
+
|
79
|
+
it 'should include default parameters' do
|
80
|
+
should match(/Timestamp/)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should sort lexicographically' do
|
84
|
+
should match(/^A1=1&A10=/)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'given an endpoint' do
|
90
|
+
before do
|
91
|
+
client.endpoint = 'http://slowapi.com/delay/0'
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#connection" do
|
95
|
+
subject { client.connection }
|
96
|
+
let(:headers) { subject.data[:headers] }
|
97
|
+
|
98
|
+
it { should be_an Excon::Connection }
|
99
|
+
|
100
|
+
it 'should set default headers' do
|
101
|
+
headers.should eq klass.headers
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'should cache itself' do
|
105
|
+
subject.should be client.connection
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'given an endpoint, key, and secret' do
|
111
|
+
before do
|
112
|
+
client.endpoint = 'http://slowapi.com/delay/0'
|
113
|
+
client.key = 'key'
|
114
|
+
client.secret = 'secret'
|
115
|
+
end
|
116
|
+
|
117
|
+
Excon::HTTP_VERBS.each do |method|
|
118
|
+
describe "##{method}" do
|
119
|
+
subject { client.send(method, mock: true).body }
|
120
|
+
|
121
|
+
before do
|
122
|
+
Excon.stub({ method: method.to_sym }) do
|
123
|
+
{ body: method, status: 200 }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
after { Excon.stubs.clear }
|
128
|
+
|
129
|
+
it "should make a #{method.upcase} request" do
|
130
|
+
should eql method
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'given a request body' do
|
136
|
+
subject { client.get(mock: true, body: 'foo').body }
|
137
|
+
|
138
|
+
before do
|
139
|
+
Excon.stub({ method: :get }) do |params|
|
140
|
+
{ status: 200, body: params[:headers]['Content-MD5'] }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
after { Excon.stubs.clear }
|
145
|
+
|
146
|
+
it "should add an Content-MD5 header" do
|
147
|
+
should_not be_empty
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
context 'given an HTTP status error' do
|
152
|
+
before do
|
153
|
+
Excon.stub({ method: :get }) do
|
154
|
+
{ status: 503 }
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
after { Excon.stubs.clear }
|
159
|
+
|
160
|
+
it "should raise an error" do
|
161
|
+
expect {
|
162
|
+
client.get mock: true
|
163
|
+
}.to raise_error Excon::Errors::HTTPStatusError
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,64 +1,64 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jeff
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.3
|
5
4
|
prerelease:
|
5
|
+
version: 0.5.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Hakan Ensari
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
15
|
+
version_requirements: !ruby/object:Gem::Requirement
|
18
16
|
requirements:
|
19
17
|
- - ~>
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0.
|
19
|
+
version: 0.18.0
|
20
|
+
none: false
|
21
|
+
name: excon
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
|
25
|
-
none: false
|
24
|
+
requirement: !ruby/object:Gem::Requirement
|
26
25
|
requirements:
|
27
26
|
- - ~>
|
28
27
|
- !ruby/object:Gem::Version
|
29
|
-
version: 0.
|
30
|
-
- !ruby/object:Gem::Dependency
|
31
|
-
name: rake
|
32
|
-
requirement: !ruby/object:Gem::Requirement
|
28
|
+
version: 0.18.0
|
33
29
|
none: false
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
version_requirements: !ruby/object:Gem::Requirement
|
34
32
|
requirements:
|
35
33
|
- - ! '>='
|
36
34
|
- !ruby/object:Gem::Version
|
37
35
|
version: '0'
|
36
|
+
none: false
|
37
|
+
name: rake
|
38
38
|
type: :development
|
39
39
|
prerelease: false
|
40
|
-
|
41
|
-
none: false
|
40
|
+
requirement: !ruby/object:Gem::Requirement
|
42
41
|
requirements:
|
43
42
|
- - ! '>='
|
44
43
|
- !ruby/object:Gem::Version
|
45
44
|
version: '0'
|
46
|
-
- !ruby/object:Gem::Dependency
|
47
|
-
name: rspec
|
48
|
-
requirement: !ruby/object:Gem::Requirement
|
49
45
|
none: false
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
48
|
requirements:
|
51
49
|
- - ! '>='
|
52
50
|
- !ruby/object:Gem::Version
|
53
51
|
version: '0'
|
52
|
+
none: false
|
53
|
+
name: rspec
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
|
-
|
57
|
-
none: false
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
58
57
|
requirements:
|
59
58
|
- - ! '>='
|
60
59
|
- !ruby/object:Gem::Version
|
61
60
|
version: '0'
|
61
|
+
none: false
|
62
62
|
description: Minimum-viable Amazon Web Services (AWS) client
|
63
63
|
email:
|
64
64
|
- hakan.ensari@papercavalier.com
|
@@ -75,10 +75,9 @@ files:
|
|
75
75
|
- jeff.gemspec
|
76
76
|
- lib/jeff.rb
|
77
77
|
- lib/jeff/secret.rb
|
78
|
-
- lib/jeff/serviceable.rb
|
79
78
|
- lib/jeff/version.rb
|
80
79
|
- spec/jeff/secret_spec.rb
|
81
|
-
- spec/
|
80
|
+
- spec/jeff_spec.rb
|
82
81
|
- spec/spec_helper.rb
|
83
82
|
homepage: https://github.com/papercavalier/jeff
|
84
83
|
licenses: []
|
@@ -87,17 +86,17 @@ rdoc_options: []
|
|
87
86
|
require_paths:
|
88
87
|
- lib
|
89
88
|
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
-
none: false
|
91
89
|
requirements:
|
92
90
|
- - ! '>='
|
93
91
|
- !ruby/object:Gem::Version
|
94
92
|
version: '1.9'
|
95
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
93
|
none: false
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
95
|
requirements:
|
98
96
|
- - ! '>='
|
99
97
|
- !ruby/object:Gem::Version
|
100
98
|
version: '0'
|
99
|
+
none: false
|
101
100
|
requirements: []
|
102
101
|
rubyforge_project:
|
103
102
|
rubygems_version: 1.8.23
|
@@ -106,6 +105,5 @@ specification_version: 3
|
|
106
105
|
summary: AWS client
|
107
106
|
test_files:
|
108
107
|
- spec/jeff/secret_spec.rb
|
109
|
-
- spec/
|
108
|
+
- spec/jeff_spec.rb
|
110
109
|
- spec/spec_helper.rb
|
111
|
-
has_rdoc:
|
data/lib/jeff/serviceable.rb
DELETED
@@ -1,184 +0,0 @@
|
|
1
|
-
require 'base64'
|
2
|
-
require 'digest/md5'
|
3
|
-
require 'excon'
|
4
|
-
require 'time'
|
5
|
-
|
6
|
-
require 'jeff/secret'
|
7
|
-
require 'jeff/version'
|
8
|
-
|
9
|
-
module Jeff
|
10
|
-
# Mixes in Amazon Web Services (AWS) client behaviour.
|
11
|
-
module Serviceable
|
12
|
-
MissingEndpoint = Class.new ArgumentError
|
13
|
-
MissingKey = Class.new ArgumentError
|
14
|
-
MissingSecret = Class.new ArgumentError
|
15
|
-
|
16
|
-
UNRESERVED = /([^\w.~-]+)/
|
17
|
-
|
18
|
-
# A User-Agent header that identifies the application, its version number,
|
19
|
-
# and programming language.
|
20
|
-
#
|
21
|
-
# Amazon recommends to include one in requests to AWS endpoints.
|
22
|
-
USER_AGENT = "Jeff/#{VERSION} (Language=Ruby; #{`hostname`.chomp})"
|
23
|
-
|
24
|
-
def self.included(base)
|
25
|
-
base.extend ClassMethods
|
26
|
-
|
27
|
-
base.headers 'User-Agent' => USER_AGENT
|
28
|
-
|
29
|
-
base.params 'AWSAccessKeyId' => -> { key },
|
30
|
-
'SignatureVersion' => '2',
|
31
|
-
'SignatureMethod' => 'HmacSHA256',
|
32
|
-
'Timestamp' => -> { Time.now.utc.iso8601 }
|
33
|
-
end
|
34
|
-
|
35
|
-
# Internal: Builds a sorted query.
|
36
|
-
#
|
37
|
-
# hsh - A hash of query parameters specific to the request.
|
38
|
-
#
|
39
|
-
# Returns a query String.
|
40
|
-
def build_query(hsh)
|
41
|
-
params
|
42
|
-
.merge(hsh)
|
43
|
-
.sort
|
44
|
-
.map { |k, v| "#{k}=#{ escape v }" }
|
45
|
-
.join '&'
|
46
|
-
end
|
47
|
-
|
48
|
-
# Internal: Returns an Excon::Connection.
|
49
|
-
def connection
|
50
|
-
@connection ||= Excon.new endpoint,
|
51
|
-
headers: headers,
|
52
|
-
expects: 200
|
53
|
-
end
|
54
|
-
|
55
|
-
# Internal: Gets the String AWS endpoint.
|
56
|
-
#
|
57
|
-
# Raises a MissingEndpoint error if endpoint is missing.
|
58
|
-
def endpoint
|
59
|
-
@endpoint or raise MissingEndpoint
|
60
|
-
end
|
61
|
-
|
62
|
-
# Sets the String AWS endpoint.
|
63
|
-
attr_writer :endpoint
|
64
|
-
|
65
|
-
# Internal: Returns the Hash default headers.
|
66
|
-
def headers
|
67
|
-
self.class.headers
|
68
|
-
end
|
69
|
-
|
70
|
-
# Internal: Gets the String AWS access key id.
|
71
|
-
#
|
72
|
-
# Raises a MissingKey error if key is missing.
|
73
|
-
def key
|
74
|
-
@key or raise MissingKey
|
75
|
-
end
|
76
|
-
|
77
|
-
# Sets the String AWS access key id.
|
78
|
-
attr_writer :key
|
79
|
-
|
80
|
-
# Internal: Returns the Hash default request parameters.
|
81
|
-
def params
|
82
|
-
self.class.params.reduce({}) do |a, (k, v)|
|
83
|
-
a.update k => (v.respond_to?(:call) ? instance_exec(&v) : v)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
# Internal: Gets the Jeff::Secret.
|
88
|
-
#
|
89
|
-
# Raises a MissingSecret error if secret is missing.
|
90
|
-
def secret
|
91
|
-
@secret or raise MissingSecret
|
92
|
-
end
|
93
|
-
|
94
|
-
# Sets the AWS secret key.
|
95
|
-
#
|
96
|
-
# key - A String secret.
|
97
|
-
#
|
98
|
-
# Returns a Jeff::Secret.
|
99
|
-
def secret=(key)
|
100
|
-
@secret = Secret.new key
|
101
|
-
end
|
102
|
-
|
103
|
-
# Generate HTTP request verb methods.
|
104
|
-
Excon::HTTP_VERBS.each do |method|
|
105
|
-
eval <<-DEF
|
106
|
-
def #{method}(opts = {})
|
107
|
-
opts.update method: :#{method}
|
108
|
-
if opts[:body]
|
109
|
-
opts[:headers] ||= {}
|
110
|
-
opts[:headers].update 'Content-MD5' => calculate_md5(opts[:body])
|
111
|
-
end
|
112
|
-
|
113
|
-
connection.request sign opts
|
114
|
-
end
|
115
|
-
DEF
|
116
|
-
end
|
117
|
-
|
118
|
-
private
|
119
|
-
|
120
|
-
def calculate_md5(body)
|
121
|
-
Base64.encode64(Digest::MD5.digest(body)).strip
|
122
|
-
end
|
123
|
-
|
124
|
-
def connection_host
|
125
|
-
[
|
126
|
-
connection.connection[:host],
|
127
|
-
connection.connection[:port]
|
128
|
-
].join ':'
|
129
|
-
end
|
130
|
-
|
131
|
-
def connection_path
|
132
|
-
connection.connection[:path]
|
133
|
-
end
|
134
|
-
|
135
|
-
def escape(val)
|
136
|
-
val.to_s.gsub(UNRESERVED) do
|
137
|
-
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def sign(opts)
|
142
|
-
query = build_query opts[:query] || {}
|
143
|
-
|
144
|
-
string_to_sign = [
|
145
|
-
opts[:method].upcase,
|
146
|
-
connection_host,
|
147
|
-
opts[:path] || connection_path,
|
148
|
-
query
|
149
|
-
].join "\n"
|
150
|
-
signature = secret.sign string_to_sign
|
151
|
-
|
152
|
-
opts.update query: [
|
153
|
-
query,
|
154
|
-
"Signature=#{escape signature}"
|
155
|
-
].join('&')
|
156
|
-
end
|
157
|
-
|
158
|
-
module ClassMethods
|
159
|
-
# Gets/Updates the default headers.
|
160
|
-
#
|
161
|
-
# hsh - A Hash of headers.
|
162
|
-
#
|
163
|
-
# Returns the Hash headers.
|
164
|
-
def headers(hsh = nil)
|
165
|
-
@headers ||= {}
|
166
|
-
@headers.update hsh if hsh
|
167
|
-
|
168
|
-
@headers
|
169
|
-
end
|
170
|
-
|
171
|
-
# Gets/Updates the default request parameters.
|
172
|
-
#
|
173
|
-
# hsh - A Hash of parameters (default: nil).
|
174
|
-
#
|
175
|
-
# Returns the Hash parameters.
|
176
|
-
def params(hsh = nil)
|
177
|
-
@params ||= {}
|
178
|
-
@params.update hsh if hsh
|
179
|
-
|
180
|
-
@params
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
@@ -1,169 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Jeff
|
4
|
-
describe Serviceable do
|
5
|
-
let(:klass) { Class.new { include Jeff } }
|
6
|
-
let(:client) { klass.new }
|
7
|
-
|
8
|
-
describe '.headers' do
|
9
|
-
subject { klass.headers }
|
10
|
-
|
11
|
-
it { should have_key 'User-Agent' }
|
12
|
-
|
13
|
-
it 'should be configurable' do
|
14
|
-
klass.instance_eval do
|
15
|
-
headers 'Foo' => 'bar'
|
16
|
-
end
|
17
|
-
|
18
|
-
should have_key 'Foo'
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
describe '.params' do
|
23
|
-
subject { klass.params }
|
24
|
-
|
25
|
-
it { should have_key 'AWSAccessKeyId' }
|
26
|
-
|
27
|
-
it { should have_key 'SignatureMethod' }
|
28
|
-
|
29
|
-
it { should have_key 'SignatureVersion' }
|
30
|
-
|
31
|
-
it { should have_key 'Timestamp' }
|
32
|
-
|
33
|
-
it 'should be configurable' do
|
34
|
-
klass.instance_eval do
|
35
|
-
params 'Foo' => 'bar'
|
36
|
-
end
|
37
|
-
|
38
|
-
should have_key 'Foo'
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
describe '#endpoint' do
|
43
|
-
it 'should require a value' do
|
44
|
-
expect { client.endpoint }.to raise_error Serviceable::MissingEndpoint
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
describe '#key' do
|
49
|
-
it 'should require a value' do
|
50
|
-
expect { client.key }.to raise_error Serviceable::MissingKey
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
describe '#secret' do
|
55
|
-
it 'should require a value' do
|
56
|
-
expect { client.secret }.to raise_error Serviceable::MissingSecret
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
context 'given a key' do
|
61
|
-
before do
|
62
|
-
client.key = 'key'
|
63
|
-
end
|
64
|
-
|
65
|
-
describe '#params' do
|
66
|
-
subject { client.params }
|
67
|
-
|
68
|
-
it 'should include the key' do
|
69
|
-
subject['AWSAccessKeyId'].should eql client.key
|
70
|
-
end
|
71
|
-
|
72
|
-
it 'should generate a timestamp' do
|
73
|
-
subject['Timestamp'].should be_a String
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
describe '#build_query' do
|
78
|
-
subject { client.build_query 'A10' => 1, 'A1' => 1 }
|
79
|
-
|
80
|
-
it 'should include default parameters' do
|
81
|
-
should match(/Timestamp/)
|
82
|
-
end
|
83
|
-
|
84
|
-
it 'should sort lexicographically' do
|
85
|
-
should match(/^A1=1&A10=/)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
context 'given an endpoint' do
|
91
|
-
before do
|
92
|
-
client.endpoint = 'http://slowapi.com/delay/0'
|
93
|
-
end
|
94
|
-
|
95
|
-
describe "#connection" do
|
96
|
-
subject { client.connection }
|
97
|
-
let(:headers) { subject.connection[:headers] }
|
98
|
-
|
99
|
-
it { should be_an Excon::Connection }
|
100
|
-
|
101
|
-
it 'should set default headers' do
|
102
|
-
headers.should eq klass.headers
|
103
|
-
end
|
104
|
-
|
105
|
-
it 'should cache itself' do
|
106
|
-
subject.should be client.connection
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
context 'given an endpoint, key, and secret' do
|
112
|
-
before do
|
113
|
-
client.endpoint = 'http://slowapi.com/delay/0'
|
114
|
-
client.key = 'key'
|
115
|
-
client.secret = 'secret'
|
116
|
-
end
|
117
|
-
|
118
|
-
Excon::HTTP_VERBS.each do |method|
|
119
|
-
describe "##{method}" do
|
120
|
-
subject { client.send(method, mock: true).body }
|
121
|
-
|
122
|
-
before do
|
123
|
-
Excon.stub({ method: method.to_sym }) do
|
124
|
-
{ body: method, status: 200 }
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
after { Excon.stubs.clear }
|
129
|
-
|
130
|
-
it "should make a #{method.upcase} request" do
|
131
|
-
should eql method
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
context 'given a request body' do
|
137
|
-
subject { client.get(mock: true, body: 'foo').body }
|
138
|
-
|
139
|
-
before do
|
140
|
-
Excon.stub({ method: :get }) do |params|
|
141
|
-
{ status: 200, body: params[:headers]['Content-MD5'] }
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
after { Excon.stubs.clear }
|
146
|
-
|
147
|
-
it "should add an Content-MD5 header" do
|
148
|
-
should_not be_empty
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
context 'given an HTTP status error' do
|
153
|
-
before do
|
154
|
-
Excon.stub({ method: :get }) do
|
155
|
-
{ status: 503 }
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
after { Excon.stubs.clear }
|
160
|
-
|
161
|
-
it "should raise an error" do
|
162
|
-
expect {
|
163
|
-
client.get mock: true
|
164
|
-
}.to raise_error Excon::Errors::HTTPStatusError
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|