aws-ses-v4 0.8.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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/CHANGELOG +59 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +80 -0
- data/LICENSE +20 -0
- data/README.erb +97 -0
- data/README.rdoc +220 -0
- data/Rakefile +83 -0
- data/TODO +3 -0
- data/VERSION +1 -0
- data/aws-ses.gemspec +102 -0
- data/lib/aws/actionmailer/ses_extension.rb +19 -0
- data/lib/aws/ses.rb +28 -0
- data/lib/aws/ses/addresses.rb +75 -0
- data/lib/aws/ses/base.rb +286 -0
- data/lib/aws/ses/extensions.rb +39 -0
- data/lib/aws/ses/info.rb +100 -0
- data/lib/aws/ses/response.rb +110 -0
- data/lib/aws/ses/send_email.rb +159 -0
- data/lib/aws/ses/version.rb +12 -0
- data/test/address_test.rb +72 -0
- data/test/base_test.rb +104 -0
- data/test/extensions_test.rb +111 -0
- data/test/fixtures.rb +89 -0
- data/test/helper.rb +63 -0
- data/test/info_test.rb +108 -0
- data/test/mocks/fake_response.rb +26 -0
- data/test/response_test.rb +26 -0
- data/test/send_email_test.rb +144 -0
- metadata +231 -0
data/test/base_test.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class BaseTest < Test::Unit::TestCase
|
4
|
+
def test_connection_established
|
5
|
+
instance = Base.new(:access_key_id => '123', :secret_access_key => 'abc')
|
6
|
+
|
7
|
+
assert_not_nil instance.instance_variable_get("@http")
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_failed_response
|
11
|
+
@base = generate_base
|
12
|
+
mock_connection(@base, {:code => 403, :body => %{
|
13
|
+
<ErrorResponse>
|
14
|
+
<Error>
|
15
|
+
<Type>
|
16
|
+
Sender
|
17
|
+
</Type>
|
18
|
+
<Code>
|
19
|
+
ValidationError
|
20
|
+
</Code>
|
21
|
+
<Message>
|
22
|
+
Value null at 'message.subject' failed to satisfy constraint: Member must not be null
|
23
|
+
</Message>
|
24
|
+
</Error>
|
25
|
+
<RequestId>
|
26
|
+
42d59b56-7407-4c4a-be0f-4c88daeea257
|
27
|
+
</RequestId>
|
28
|
+
</ErrorResponse>
|
29
|
+
}})
|
30
|
+
|
31
|
+
assert_raises ResponseError do
|
32
|
+
result = @base.request('', {})
|
33
|
+
end
|
34
|
+
|
35
|
+
# assert !result.success?
|
36
|
+
# assert result.error?
|
37
|
+
# assert result.error.error?
|
38
|
+
# assert_equal 'ValidationError', result.error.code
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_ses_authorization_header_v2
|
42
|
+
aws_access_key_id = 'fake_aws_key_id'
|
43
|
+
aws_secret_access_key = 'fake_aws_access_key'
|
44
|
+
time = Time.new(2021, 4, 8, 14, 07, 58, '+00:00')
|
45
|
+
::Timecop.freeze(time)
|
46
|
+
base = ::AWS::SES::Base.new(
|
47
|
+
access_key_id: aws_access_key_id,
|
48
|
+
secret_access_key: aws_secret_access_key,
|
49
|
+
signature_version: 2
|
50
|
+
)
|
51
|
+
|
52
|
+
assert_equal 'AWS3-HTTPS AWSAccessKeyId=fake_aws_key_id, Algorithm=HmacSHA256, Signature=Yi984ReKUv7ckcrwvsBgzLlQX/1kyva74+SF+PlrwZ0=', base.get_aws_auth_header_v2
|
53
|
+
Timecop.return
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_ses_authorization_header_v4
|
57
|
+
aws_access_key_id = 'fake_aws_key_id'
|
58
|
+
aws_secret_access_key = 'fake_aws_access_key'
|
59
|
+
time = Time.new(2020, 7, 2, 7, 17, 58, '+00:00')
|
60
|
+
::Timecop.freeze(time)
|
61
|
+
|
62
|
+
base = ::AWS::SES::Base.new(
|
63
|
+
server: 'ec2.amazonaws.com',
|
64
|
+
access_key_id: aws_access_key_id,
|
65
|
+
secret_access_key: aws_secret_access_key
|
66
|
+
)
|
67
|
+
|
68
|
+
assert_equal 'AWS4-HMAC-SHA256 Credential=fake_aws_key_id/20200702/us-east-1/ses/aws4_request, SignedHeaders=host;x-amz-date, Signature=fb25a4a91c6f5c17637c81744998ac664d16ce70982c5ca92c0a62199ef4e7d6', base.get_aws_auth_header_v4
|
69
|
+
Timecop.return
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_ses_authorization_header_v4_changed_host
|
73
|
+
aws_access_key_id = 'fake_aws_key_id'
|
74
|
+
aws_secret_access_key = 'fake_aws_access_key'
|
75
|
+
time = Time.new(2020, 7, 2, 7, 17, 58, '+00:00')
|
76
|
+
::Timecop.freeze(time)
|
77
|
+
|
78
|
+
base = ::AWS::SES::Base.new(
|
79
|
+
server: 'email.us-east-1.amazonaws.com',
|
80
|
+
access_key_id: aws_access_key_id,
|
81
|
+
secret_access_key: aws_secret_access_key
|
82
|
+
)
|
83
|
+
|
84
|
+
assert_equal 'AWS4-HMAC-SHA256 Credential=fake_aws_key_id/20200702/us-east-1/ses/aws4_request, SignedHeaders=host;x-amz-date, Signature=c77a9d002342ac5b210c8f4a054712f9e00335ce9555c359ea8982acfa32db4c', base.get_aws_auth_header_v4
|
85
|
+
Timecop.return
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_ses_authorization_header_v4_changed_region
|
89
|
+
aws_access_key_id = 'fake_aws_key_id'
|
90
|
+
aws_secret_access_key = 'fake_aws_access_key'
|
91
|
+
time = Time.new(2020, 7, 2, 7, 17, 58, '+00:00')
|
92
|
+
::Timecop.freeze(time)
|
93
|
+
|
94
|
+
base = ::AWS::SES::Base.new(
|
95
|
+
server: 'email.us-east-1.amazonaws.com',
|
96
|
+
access_key_id: aws_access_key_id,
|
97
|
+
secret_access_key: aws_secret_access_key,
|
98
|
+
region: 'eu-west-1'
|
99
|
+
)
|
100
|
+
|
101
|
+
assert_not_equal 'AWS4-HMAC-SHA256 Credential=fake_aws_key_id/20200702/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-date, Signature=b872601457070ab98e7038bdcd4dc1f5eab586ececf9908525474408b0740515', base.get_aws_auth_header_v4
|
102
|
+
Timecop.return
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class KerneltExtensionsTest < Test::Unit::TestCase
|
4
|
+
class Foo
|
5
|
+
def foo
|
6
|
+
__method__
|
7
|
+
end
|
8
|
+
|
9
|
+
def bar
|
10
|
+
foo
|
11
|
+
end
|
12
|
+
|
13
|
+
def baz
|
14
|
+
bar
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Bar
|
19
|
+
def foo
|
20
|
+
calling_method
|
21
|
+
end
|
22
|
+
|
23
|
+
def bar
|
24
|
+
calling_method
|
25
|
+
end
|
26
|
+
|
27
|
+
def calling_method
|
28
|
+
__method__(1)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def test___method___works_regardless_of_nesting
|
33
|
+
f = Foo.new
|
34
|
+
[:foo, :bar, :baz].each do |method|
|
35
|
+
assert_equal 'foo', f.send(method)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def test___method___depth
|
40
|
+
b = Bar.new
|
41
|
+
assert_equal 'foo', b.foo
|
42
|
+
assert_equal 'bar', b.bar
|
43
|
+
end
|
44
|
+
end if RUBY_VERSION <= '1.8.7'
|
45
|
+
|
46
|
+
class ModuleExtensionsTest < Test::Unit::TestCase
|
47
|
+
class Foo
|
48
|
+
def foo(reload = false)
|
49
|
+
expirable_memoize(reload) do
|
50
|
+
Time.now
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def bar(reload = false)
|
55
|
+
expirable_memoize(reload, :baz) do
|
56
|
+
Time.now
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def quux
|
61
|
+
Time.now
|
62
|
+
end
|
63
|
+
memoized :quux
|
64
|
+
end
|
65
|
+
|
66
|
+
def setup
|
67
|
+
@instance = Foo.new
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_memoize
|
71
|
+
assert !instance_variables_of(@instance).include?('@foo')
|
72
|
+
cached_result = @instance.foo
|
73
|
+
assert_equal cached_result, @instance.foo
|
74
|
+
assert instance_variables_of(@instance).include?('@foo')
|
75
|
+
assert_equal cached_result, @instance.send(:instance_variable_get, :@foo)
|
76
|
+
assert_not_equal cached_result, new_cache = @instance.foo(:reload)
|
77
|
+
assert_equal new_cache, @instance.foo
|
78
|
+
assert_equal new_cache, @instance.send(:instance_variable_get, :@foo)
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_customizing_memoize_storage
|
82
|
+
assert !instance_variables_of(@instance).include?('@bar')
|
83
|
+
assert !instance_variables_of(@instance).include?('@baz')
|
84
|
+
cached_result = @instance.bar
|
85
|
+
assert !instance_variables_of(@instance).include?('@bar')
|
86
|
+
assert instance_variables_of(@instance).include?('@baz')
|
87
|
+
assert_equal cached_result, @instance.bar
|
88
|
+
assert_equal cached_result, @instance.send(:instance_variable_get, :@baz)
|
89
|
+
assert_nil @instance.send(:instance_variable_get, :@bar)
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_memoized
|
93
|
+
assert !instance_variables_of(@instance).include?('@quux')
|
94
|
+
cached_result = @instance.quux
|
95
|
+
assert_equal cached_result, @instance.quux
|
96
|
+
assert instance_variables_of(@instance).include?('@quux')
|
97
|
+
assert_equal cached_result, @instance.send(:instance_variable_get, :@quux)
|
98
|
+
assert_not_equal cached_result, new_cache = @instance.quux(:reload)
|
99
|
+
assert_equal new_cache, @instance.quux
|
100
|
+
assert_equal new_cache, @instance.send(:instance_variable_get, :@quux)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
# For 1.9 compatibility
|
105
|
+
def instance_variables_of(object)
|
106
|
+
object.instance_variables.map do |instance_variable|
|
107
|
+
instance_variable.to_s
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
data/test/fixtures.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module AWS
|
4
|
+
module SES
|
5
|
+
# When this file is loaded, for each fixture file, a module is created within the Fixtures module
|
6
|
+
# with the same name as the fixture file. For each fixture in that fixture file, a singleton method is
|
7
|
+
# added to the module with the name of the given fixture, returning the value of the fixture.
|
8
|
+
#
|
9
|
+
# For example:
|
10
|
+
#
|
11
|
+
# A fixture in <tt>buckets.yml</tt> named <tt>empty_bucket_list</tt> with value <tt><foo>hi!</foo></tt>
|
12
|
+
# would be made available like so:
|
13
|
+
#
|
14
|
+
# Fixtures::Buckets.empty_bucket_list
|
15
|
+
# => "<foo>hi!</foo>"
|
16
|
+
#
|
17
|
+
# Alternatively you can treat the fixture module like a hash
|
18
|
+
#
|
19
|
+
# Fixtures::Buckets[:empty_bucket_list]
|
20
|
+
# => "<foo>hi!</foo>"
|
21
|
+
#
|
22
|
+
# You can find out all available fixtures by calling
|
23
|
+
#
|
24
|
+
# Fixtures.fixtures
|
25
|
+
# => ["Buckets"]
|
26
|
+
#
|
27
|
+
# And all the fixtures contained in a given fixture by calling
|
28
|
+
#
|
29
|
+
# Fixtures::Buckets.fixtures
|
30
|
+
# => ["bucket_list_with_more_than_one_bucket", "bucket_list_with_one_bucket", "empty_bucket_list"]
|
31
|
+
module Fixtures
|
32
|
+
class << self
|
33
|
+
def create_fixtures
|
34
|
+
files.each do |file|
|
35
|
+
create_fixture_for(file)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_fixture_for(file)
|
40
|
+
fixtures = YAML.load_file(path(file))
|
41
|
+
fixture_module = Module.new
|
42
|
+
|
43
|
+
fixtures.each do |name, value|
|
44
|
+
fixture_module.module_eval(<<-EVAL, __FILE__, __LINE__)
|
45
|
+
def #{name}
|
46
|
+
#{value.inspect}
|
47
|
+
end
|
48
|
+
module_function :#{name}
|
49
|
+
EVAL
|
50
|
+
end
|
51
|
+
|
52
|
+
fixture_module.module_eval(<<-EVAL, __FILE__, __LINE__)
|
53
|
+
module_function
|
54
|
+
|
55
|
+
def fixtures
|
56
|
+
#{fixtures.keys.sort.inspect}
|
57
|
+
end
|
58
|
+
|
59
|
+
def [](name)
|
60
|
+
send(name) if fixtures.include?(name.to_s)
|
61
|
+
end
|
62
|
+
EVAL
|
63
|
+
|
64
|
+
const_set(module_name(file), fixture_module)
|
65
|
+
end
|
66
|
+
|
67
|
+
def fixtures
|
68
|
+
constants.sort
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def files
|
74
|
+
Dir.glob(File.dirname(__FILE__) + '/fixtures/*.yml').map {|fixture| File.basename(fixture)}
|
75
|
+
end
|
76
|
+
|
77
|
+
def module_name(file_name)
|
78
|
+
File.basename(file_name, '.*').capitalize
|
79
|
+
end
|
80
|
+
|
81
|
+
def path(file_name)
|
82
|
+
File.join(File.dirname(__FILE__), 'fixtures', file_name)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
create_fixtures
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
require 'shoulda-context'
|
12
|
+
|
13
|
+
begin
|
14
|
+
require 'ruby-debug'
|
15
|
+
rescue LoadError
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'flexmock'
|
19
|
+
require 'flexmock/test_unit'
|
20
|
+
|
21
|
+
require File.dirname(__FILE__) + '/mocks/fake_response'
|
22
|
+
require File.dirname(__FILE__) + '/fixtures'
|
23
|
+
|
24
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
25
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
26
|
+
require 'aws/ses'
|
27
|
+
require 'timecop'
|
28
|
+
|
29
|
+
class Test::Unit::TestCase
|
30
|
+
require 'net/http'
|
31
|
+
require 'net/https'
|
32
|
+
|
33
|
+
include AWS::SES
|
34
|
+
|
35
|
+
def mock_connection(object, data = {})
|
36
|
+
return_values = case data
|
37
|
+
when Hash
|
38
|
+
FakeResponse.new(data)
|
39
|
+
when Array
|
40
|
+
data.map {|hash| FakeResponse.new(hash)}
|
41
|
+
else
|
42
|
+
abort "Response data for mock connection must be a Hash or an Array. Was #{data.inspect}."
|
43
|
+
end
|
44
|
+
|
45
|
+
connection = flexmock('Net::HTTP.new') do |mock|
|
46
|
+
mock.should_receive(:post).and_return(*return_values).at_least.once
|
47
|
+
end
|
48
|
+
|
49
|
+
mock = flexmock(object)
|
50
|
+
mock.should_receive(:connection).and_return(connection)
|
51
|
+
mock
|
52
|
+
end
|
53
|
+
|
54
|
+
def generate_base
|
55
|
+
Base.new(:access_key_id=>'123', :secret_access_key=>'abc')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Deals w/ http://github.com/thoughtbot/shoulda/issues/issue/117, see
|
60
|
+
# http://stackoverflow.com/questions/3657972/nameerror-uninitialized-constant-testunitassertionfailederror-when-upgradin
|
61
|
+
unless defined?(Test::Unit::AssertionFailedError)
|
62
|
+
Test::Unit::AssertionFailedError = ActiveSupport::TestCase::Assertion
|
63
|
+
end
|
data/test/info_test.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class InfoTest < Test::Unit::TestCase
|
4
|
+
context 'getting send quota' do
|
5
|
+
setup do
|
6
|
+
@base = generate_base
|
7
|
+
end
|
8
|
+
|
9
|
+
should 'return the correct response on success' do
|
10
|
+
mock_connection(@base, :body => %{
|
11
|
+
<GetSendQuotaResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
12
|
+
<GetSendQuotaResult>
|
13
|
+
<SentLast24Hours>0.0</SentLast24Hours>
|
14
|
+
<Max24HourSend>1000.0</Max24HourSend>
|
15
|
+
<MaxSendRate>1.0</MaxSendRate>
|
16
|
+
</GetSendQuotaResult>
|
17
|
+
<ResponseMetadata>
|
18
|
+
<RequestId>abc-123</RequestId>
|
19
|
+
</ResponseMetadata>
|
20
|
+
</GetSendQuotaResponse>
|
21
|
+
})
|
22
|
+
|
23
|
+
result = @base.quota
|
24
|
+
assert result.success?
|
25
|
+
assert_equal 'abc-123', result.request_id
|
26
|
+
assert_equal '0.0', result.sent_last_24_hours
|
27
|
+
assert_equal '1000.0', result.max_24_hour_send
|
28
|
+
assert_equal '1.0', result.max_send_rate
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'getting send statistics' do
|
33
|
+
setup do
|
34
|
+
@base = generate_base
|
35
|
+
end
|
36
|
+
|
37
|
+
should 'return the correct response on success' do
|
38
|
+
mock_connection(@base, :body => %{
|
39
|
+
<GetSendStatisticsResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
40
|
+
<GetSendStatisticsResult>
|
41
|
+
<SendDataPoints>
|
42
|
+
<member>
|
43
|
+
<DeliveryAttempts>3</DeliveryAttempts>
|
44
|
+
<Timestamp>2011-01-31T15:31:00Z</Timestamp>
|
45
|
+
<Rejects>2</Rejects>
|
46
|
+
<Bounces>4</Bounces>
|
47
|
+
<Complaints>1</Complaints>
|
48
|
+
</member>
|
49
|
+
<member>
|
50
|
+
<DeliveryAttempts>3</DeliveryAttempts>
|
51
|
+
<Timestamp>2011-01-31T16:01:00Z</Timestamp>
|
52
|
+
<Rejects>0</Rejects>
|
53
|
+
<Bounces>0</Bounces>
|
54
|
+
<Complaints>0</Complaints>
|
55
|
+
</member>
|
56
|
+
<member>
|
57
|
+
<DeliveryAttempts>1</DeliveryAttempts>
|
58
|
+
<Timestamp>2011-01-26T16:31:00Z</Timestamp>
|
59
|
+
<Rejects>0</Rejects>
|
60
|
+
<Bounces>0</Bounces>
|
61
|
+
<Complaints>0</Complaints>
|
62
|
+
</member>
|
63
|
+
</SendDataPoints>
|
64
|
+
</GetSendStatisticsResult>
|
65
|
+
<ResponseMetadata>
|
66
|
+
<RequestId>abc-123</RequestId>
|
67
|
+
</ResponseMetadata>
|
68
|
+
</GetSendStatisticsResponse>
|
69
|
+
})
|
70
|
+
|
71
|
+
result = @base.statistics
|
72
|
+
|
73
|
+
assert result.success?
|
74
|
+
assert_equal 'abc-123', result.request_id
|
75
|
+
|
76
|
+
assert_equal 3, result.data_points.size
|
77
|
+
|
78
|
+
d = result.data_points.first
|
79
|
+
|
80
|
+
assert_equal '2', d['Rejects']
|
81
|
+
assert_equal '3', d['DeliveryAttempts']
|
82
|
+
assert_equal '4', d['Bounces']
|
83
|
+
assert_equal '1', d['Complaints']
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
context 'deleting a verified addressess' do
|
89
|
+
setup do
|
90
|
+
@base = generate_base
|
91
|
+
end
|
92
|
+
|
93
|
+
should 'return the correct response on success' do
|
94
|
+
mock_connection(@base, :body => %{
|
95
|
+
<DeleteVerifiedEmailAddressResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
96
|
+
<ResponseMetadata>
|
97
|
+
<RequestId>abc-123</RequestId>
|
98
|
+
</ResponseMetadata>
|
99
|
+
</DeleteVerifiedEmailAddressResponse>
|
100
|
+
})
|
101
|
+
|
102
|
+
result = @base.addresses.delete('user1@example.com')
|
103
|
+
|
104
|
+
assert result.success?
|
105
|
+
assert_equal 'abc-123', result.request_id
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|