aws-ses 0.4.3 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG +17 -0
- data/Gemfile +8 -6
- data/Gemfile.lock +63 -22
- data/README.erb +8 -5
- data/README.rdoc +35 -13
- data/Rakefile +22 -21
- data/VERSION +1 -1
- data/aws-ses.gemspec +48 -51
- data/lib/aws/ses.rb +2 -2
- data/lib/aws/ses/base.rb +111 -16
- data/lib/aws/ses/send_email.rb +45 -30
- data/lib/aws/ses/version.rb +1 -1
- data/test/base_test.rb +65 -0
- data/test/helper.rb +7 -0
- data/test/send_email_test.rb +86 -9
- metadata +160 -168
data/lib/aws/ses.rb
CHANGED
data/lib/aws/ses/base.rb
CHANGED
@@ -17,11 +17,31 @@ module AWS #:nodoc:
|
|
17
17
|
# )
|
18
18
|
#
|
19
19
|
# The minimum connection options that you must specify are your access key id and your secret access key.
|
20
|
+
#
|
21
|
+
# === Connecting to a server from another region
|
22
|
+
#
|
23
|
+
# The default server API endpoint is "email.us-east-1.amazonaws.com", corresponding to the US East 1 region.
|
24
|
+
# To connect to a different one, just pass it as a parameter to the AWS::SES::Base initializer:
|
25
|
+
#
|
26
|
+
# ses = AWS::SES::Base.new(
|
27
|
+
# :access_key_id => 'abc',
|
28
|
+
# :secret_access_key => '123',
|
29
|
+
# :server => 'email.eu-west-1.amazonaws.com',
|
30
|
+
# :message_id_domain => 'eu-west-1.amazonses.com'
|
31
|
+
# )
|
32
|
+
#
|
33
|
+
|
20
34
|
module SES
|
21
35
|
|
22
36
|
API_VERSION = '2010-12-01'
|
23
|
-
|
37
|
+
|
38
|
+
DEFAULT_REGION = 'us-east-1'
|
39
|
+
|
40
|
+
SERVICE = 'ec2'
|
41
|
+
|
24
42
|
DEFAULT_HOST = 'email.us-east-1.amazonaws.com'
|
43
|
+
|
44
|
+
DEFAULT_MESSAGE_ID_DOMAIN = 'email.amazonses.com'
|
25
45
|
|
26
46
|
USER_AGENT = 'github-aws-ses-ruby-gem'
|
27
47
|
|
@@ -35,7 +55,7 @@ module AWS #:nodoc:
|
|
35
55
|
# @param [Boolean] urlencode whether or not to url encode the result., true or false
|
36
56
|
# @return [String] the signed and encoded string.
|
37
57
|
def SES.encode(secret_access_key, str, urlencode=true)
|
38
|
-
digest = OpenSSL::Digest
|
58
|
+
digest = OpenSSL::Digest.new('sha256')
|
39
59
|
b64_hmac =
|
40
60
|
Base64.encode64(
|
41
61
|
OpenSSL::HMAC.digest(digest, secret_access_key, str)).gsub("\n","")
|
@@ -55,13 +75,18 @@ module AWS #:nodoc:
|
|
55
75
|
def SES.authorization_header(key, alg, sig)
|
56
76
|
"AWS3-HTTPS AWSAccessKeyId=#{key}, Algorithm=#{alg}, Signature=#{sig}"
|
57
77
|
end
|
58
|
-
|
78
|
+
|
79
|
+
def SES.authorization_header_v4(credential, signed_headers, signature)
|
80
|
+
"AWS4-HMAC-SHA256 Credential=#{credential}, SignedHeaders=#{signed_headers}, Signature=#{signature}"
|
81
|
+
end
|
82
|
+
|
59
83
|
# AWS::SES::Base is the abstract super class of all classes who make requests against SES
|
60
84
|
class Base
|
61
85
|
include SendEmail
|
62
86
|
include Info
|
63
87
|
|
64
|
-
attr_reader :use_ssl, :server, :proxy_server, :port
|
88
|
+
attr_reader :use_ssl, :server, :proxy_server, :port, :message_id_domain, :signature_version, :region
|
89
|
+
attr_accessor :settings
|
65
90
|
|
66
91
|
# @option options [String] :access_key_id ("") The user's AWS Access Key ID
|
67
92
|
# @option options [String] :secret_access_key ("") The user's AWS Secret Access Key
|
@@ -69,6 +94,8 @@ module AWS #:nodoc:
|
|
69
94
|
# @option options [String] :server ("email.us-east-1.amazonaws.com") The server API endpoint host
|
70
95
|
# @option options [String] :proxy_server (nil) An HTTP proxy server FQDN
|
71
96
|
# @option options [String] :user_agent ("github-aws-ses-ruby-gem") The HTTP User-Agent header value
|
97
|
+
# @option options [String] :region ("us-east-1") The server API endpoint host
|
98
|
+
# @option options [String] :message_id_domain ("us-east-1.amazonses.com") Domain used to build message_id header
|
72
99
|
# @return [Object] the object.
|
73
100
|
def initialize( options = {} )
|
74
101
|
|
@@ -76,16 +103,22 @@ module AWS #:nodoc:
|
|
76
103
|
:secret_access_key => "",
|
77
104
|
:use_ssl => true,
|
78
105
|
:server => DEFAULT_HOST,
|
106
|
+
:message_id_domain => DEFAULT_MESSAGE_ID_DOMAIN,
|
79
107
|
:path => "/",
|
80
108
|
:user_agent => USER_AGENT,
|
81
|
-
:proxy_server => nil
|
109
|
+
:proxy_server => nil,
|
110
|
+
:region => DEFAULT_REGION
|
82
111
|
}.merge(options)
|
83
112
|
|
113
|
+
@signature_version = options[:signature_version] || 2
|
84
114
|
@server = options[:server]
|
115
|
+
@message_id_domain = options[:message_id_domain]
|
85
116
|
@proxy_server = options[:proxy_server]
|
86
117
|
@use_ssl = options[:use_ssl]
|
87
118
|
@path = options[:path]
|
88
119
|
@user_agent = options[:user_agent]
|
120
|
+
@region = options[:region]
|
121
|
+
@settings = {}
|
89
122
|
|
90
123
|
raise ArgumentError, "No :access_key_id provided" if options[:access_key_id].nil? || options[:access_key_id].empty?
|
91
124
|
raise ArgumentError, "No :secret_access_key provided" if options[:secret_access_key].nil? || options[:secret_access_key].empty?
|
@@ -116,14 +149,8 @@ module AWS #:nodoc:
|
|
116
149
|
proxy.password).new(options[:server], @port)
|
117
150
|
|
118
151
|
@http.use_ssl = @use_ssl
|
119
|
-
|
120
|
-
# Don't verify the SSL certificates. Avoids SSL Cert warning in log on every GET.
|
121
|
-
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
122
|
-
|
123
152
|
end
|
124
153
|
|
125
|
-
attr_accessor :settings
|
126
|
-
|
127
154
|
def connection
|
128
155
|
@http
|
129
156
|
end
|
@@ -138,7 +165,7 @@ module AWS #:nodoc:
|
|
138
165
|
timestamp = Time.now.getutc
|
139
166
|
|
140
167
|
params.merge!( {"Action" => action,
|
141
|
-
"SignatureVersion" =>
|
168
|
+
"SignatureVersion" => signature_version.to_s,
|
142
169
|
"SignatureMethod" => 'HmacSHA256',
|
143
170
|
"AWSAccessKeyId" => @access_key_id,
|
144
171
|
"Version" => API_VERSION,
|
@@ -150,9 +177,9 @@ module AWS #:nodoc:
|
|
150
177
|
|
151
178
|
req = {}
|
152
179
|
|
153
|
-
req['X-Amzn-Authorization'] = get_aws_auth_param(timestamp.httpdate, @secret_access_key)
|
180
|
+
req['X-Amzn-Authorization'] = get_aws_auth_param(timestamp.httpdate, @secret_access_key, action, signature_version)
|
154
181
|
req['Date'] = timestamp.httpdate
|
155
|
-
req['User-Agent'] = @user_agent
|
182
|
+
req['User-Agent'] = @user_agent
|
156
183
|
|
157
184
|
response = connection.post(@path, query, req)
|
158
185
|
|
@@ -167,9 +194,77 @@ module AWS #:nodoc:
|
|
167
194
|
end
|
168
195
|
|
169
196
|
# Set the Authorization header using AWS signed header authentication
|
170
|
-
def get_aws_auth_param(timestamp, secret_access_key)
|
197
|
+
def get_aws_auth_param(timestamp, secret_access_key, action = '', signature_version = 2)
|
198
|
+
raise(ArgumentError, "signature_version must be `2` or `4`") unless signature_version == 2 || signature_version == 4
|
171
199
|
encoded_canonical = SES.encode(secret_access_key, timestamp, false)
|
172
|
-
|
200
|
+
|
201
|
+
if signature_version == 4
|
202
|
+
SES.authorization_header_v4(sig_v4_auth_credential, sig_v4_auth_signed_headers, sig_v4_auth_signature(action))
|
203
|
+
else
|
204
|
+
SES.authorization_header(@access_key_id, 'HmacSHA256', encoded_canonical)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
def sig_v4_auth_credential
|
211
|
+
@access_key_id + '/' + credential_scope
|
212
|
+
end
|
213
|
+
|
214
|
+
def sig_v4_auth_signed_headers
|
215
|
+
'host;x-amz-date'
|
216
|
+
end
|
217
|
+
|
218
|
+
def credential_scope
|
219
|
+
datestamp + '/' + region + '/' + SERVICE + '/' + 'aws4_request'
|
220
|
+
end
|
221
|
+
|
222
|
+
def string_to_sign(for_action)
|
223
|
+
"AWS4-HMAC-SHA256\n" + amzdate + "\n" + credential_scope + "\n" + Digest::SHA256.hexdigest(canonical_request(for_action).encode('utf-8').b)
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
def amzdate
|
228
|
+
Time.now.utc.strftime('%Y%m%dT%H%M%SZ')
|
229
|
+
end
|
230
|
+
|
231
|
+
def datestamp
|
232
|
+
Time.now.utc.strftime('%Y%m%d')
|
233
|
+
end
|
234
|
+
|
235
|
+
def canonical_request(for_action)
|
236
|
+
"GET" + "\n" + "/" + "\n" + canonical_querystring(for_action) + "\n" + canonical_headers + "\n" + sig_v4_auth_signed_headers + "\n" + payload_hash
|
237
|
+
end
|
238
|
+
|
239
|
+
def canonical_querystring(action)
|
240
|
+
"Action=#{action}&Version=2013-10-15"
|
241
|
+
end
|
242
|
+
|
243
|
+
def canonical_headers
|
244
|
+
'host:' + server + "\n" + 'x-amz-date:' + amzdate + "\n"
|
245
|
+
end
|
246
|
+
|
247
|
+
def payload_hash
|
248
|
+
Digest::SHA256.hexdigest(''.encode('utf-8'))
|
249
|
+
end
|
250
|
+
|
251
|
+
def sig_v4_auth_signature(for_action)
|
252
|
+
signing_key = getSignatureKey(@secret_access_key, datestamp, region, SERVICE)
|
253
|
+
|
254
|
+
OpenSSL::HMAC.hexdigest("SHA256", signing_key, string_to_sign(for_action).encode('utf-8'))
|
255
|
+
end
|
256
|
+
|
257
|
+
def getSignatureKey(key, dateStamp, regionName, serviceName)
|
258
|
+
kDate = sign(('AWS4' + key).encode('utf-8'), dateStamp)
|
259
|
+
kRegion = sign(kDate, regionName)
|
260
|
+
kService = sign(kRegion, serviceName)
|
261
|
+
kSigning = sign(kService, 'aws4_request')
|
262
|
+
|
263
|
+
kSigning
|
264
|
+
end
|
265
|
+
|
266
|
+
def sign(key, msg)
|
267
|
+
OpenSSL::HMAC.digest("SHA256", key, msg.encode('utf-8'))
|
173
268
|
end
|
174
269
|
end # class Base
|
175
270
|
end # Module SES
|
data/lib/aws/ses/send_email.rb
CHANGED
@@ -9,24 +9,24 @@ module AWS
|
|
9
9
|
# :subject => 'Subject Line'
|
10
10
|
# :text_body => 'Internal text body'
|
11
11
|
#
|
12
|
-
# By default, the email "from" display address is whatever is before the @.
|
13
|
-
# To change the display from, use the format:
|
12
|
+
# By default, the email "from" display address is whatever is before the @.
|
13
|
+
# To change the display from, use the format:
|
14
14
|
#
|
15
15
|
# "Steve Smith" <steve@example.com>
|
16
16
|
#
|
17
17
|
# You can also send Mail objects using send_raw_email:
|
18
|
-
#
|
18
|
+
#
|
19
19
|
# m = Mail.new( :to => ..., :from => ... )
|
20
20
|
# ses.send_raw_email(m)
|
21
21
|
#
|
22
22
|
# send_raw_email will also take a hash and pass it through Mail.new automatically as well.
|
23
23
|
#
|
24
24
|
module SendEmail
|
25
|
-
|
25
|
+
|
26
26
|
# Sends an email through SES
|
27
|
-
#
|
27
|
+
#
|
28
28
|
# the destination parameters can be:
|
29
|
-
#
|
29
|
+
#
|
30
30
|
# [A single e-mail string] "jon@example.com"
|
31
31
|
# [A array of e-mail addresses] ['jon@example.com', 'dave@example.com']
|
32
32
|
#
|
@@ -51,31 +51,31 @@ module AWS
|
|
51
51
|
# @return [Response] the response to sending this e-mail
|
52
52
|
def send_email(options = {})
|
53
53
|
package = {}
|
54
|
-
|
54
|
+
|
55
55
|
package['Source'] = options[:source] || options[:from]
|
56
|
-
|
56
|
+
|
57
57
|
add_array_to_hash!(package, 'Destination.ToAddresses', options[:to]) if options[:to]
|
58
58
|
add_array_to_hash!(package, 'Destination.CcAddresses', options[:cc]) if options[:cc]
|
59
59
|
add_array_to_hash!(package, 'Destination.BccAddresses', options[:bcc]) if options[:bcc]
|
60
|
-
|
60
|
+
|
61
61
|
package['Message.Subject.Data'] = options[:subject]
|
62
|
-
|
62
|
+
|
63
63
|
package['Message.Body.Html.Data'] = options[:html_body] if options[:html_body]
|
64
64
|
package['Message.Body.Text.Data'] = options[:text_body] || options[:body] if options[:text_body] || options[:body]
|
65
|
-
|
65
|
+
|
66
66
|
package['ReturnPath'] = options[:return_path] if options[:return_path]
|
67
|
-
|
67
|
+
|
68
68
|
add_array_to_hash!(package, 'ReplyToAddresses', options[:reply_to]) if options[:reply_to]
|
69
|
-
|
69
|
+
|
70
70
|
request('SendEmail', package)
|
71
71
|
end
|
72
|
-
|
72
|
+
|
73
73
|
# Sends using the SendRawEmail method
|
74
74
|
# This gives the most control and flexibility
|
75
75
|
#
|
76
76
|
# This uses the underlying Mail object from the mail gem
|
77
77
|
# You can pass in a Mail object, a Hash of params that will be parsed by Mail.new, or just a string
|
78
|
-
#
|
78
|
+
#
|
79
79
|
# Note that the params are different from send_email
|
80
80
|
# Specifically, the following fields from send_email will NOT work:
|
81
81
|
#
|
@@ -96,23 +96,38 @@ module AWS
|
|
96
96
|
# @option args [String] :to alias for :destinations
|
97
97
|
# @return [Response]
|
98
98
|
def send_raw_email(mail, args = {})
|
99
|
-
message = mail.is_a?(Hash) ? Mail.new(mail)
|
100
|
-
|
99
|
+
message = mail.is_a?(Hash) ? Mail.new(mail) : mail
|
100
|
+
raise ArgumentError, "Attachment provided without message body" if message.has_attachments? && message.text_part.nil? && message.html_part.nil?
|
101
|
+
|
102
|
+
raw_email = build_raw_email(message, args)
|
103
|
+
result = request('SendRawEmail', raw_email)
|
104
|
+
message.message_id = "<#{result.parsed['SendRawEmailResult']['MessageId']}@#{message_id_domain}>"
|
105
|
+
result
|
106
|
+
end
|
107
|
+
|
108
|
+
alias :deliver! :send_raw_email
|
109
|
+
alias :deliver :send_raw_email
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def build_raw_email(message, args = {})
|
114
|
+
# the message.to_s includes the :to and :cc addresses
|
115
|
+
package = { 'RawMessage.Data' => Base64::encode64(message.to_s) }
|
101
116
|
package['Source'] = args[:from] if args[:from]
|
102
117
|
package['Source'] = args[:source] if args[:source]
|
103
118
|
if args[:destinations]
|
104
119
|
add_array_to_hash!(package, 'Destinations', args[:destinations])
|
105
120
|
else
|
106
|
-
|
121
|
+
mail_addresses = [message.to, message.cc, message.bcc].flatten.compact
|
122
|
+
args_addresses = [args[:to], args[:cc], args[:bcc]].flatten.compact
|
123
|
+
|
124
|
+
mail_addresses = args_addresses unless args_addresses.empty?
|
125
|
+
|
126
|
+
add_array_to_hash!(package, 'Destinations', mail_addresses)
|
107
127
|
end
|
108
|
-
|
128
|
+
package
|
109
129
|
end
|
110
130
|
|
111
|
-
alias :deliver! :send_raw_email
|
112
|
-
alias :deliver :send_raw_email
|
113
|
-
|
114
|
-
private
|
115
|
-
|
116
131
|
# Adds all elements of the ary with the appropriate member elements
|
117
132
|
def add_array_to_hash!(hash, key, ary)
|
118
133
|
cnt = 1
|
@@ -122,23 +137,23 @@ module AWS
|
|
122
137
|
end
|
123
138
|
end
|
124
139
|
end
|
125
|
-
|
140
|
+
|
126
141
|
class EmailResponse < AWS::SES::Response
|
127
142
|
def result
|
128
143
|
super["#{action}Result"]
|
129
144
|
end
|
130
|
-
|
145
|
+
|
131
146
|
def message_id
|
132
147
|
result['MessageId']
|
133
148
|
end
|
134
149
|
end
|
135
|
-
|
150
|
+
|
136
151
|
class SendEmailResponse < EmailResponse
|
137
|
-
|
152
|
+
|
138
153
|
end
|
139
|
-
|
154
|
+
|
140
155
|
class SendRawEmailResponse < EmailResponse
|
141
|
-
|
156
|
+
|
142
157
|
end
|
143
158
|
end
|
144
159
|
end
|
data/lib/aws/ses/version.rb
CHANGED
data/test/base_test.rb
CHANGED
@@ -37,4 +37,69 @@ class BaseTest < Test::Unit::TestCase
|
|
37
37
|
# assert result.error.error?
|
38
38
|
# assert_equal 'ValidationError', result.error.code
|
39
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
|
+
timestamp = Time.new(2020, 7, 2, 7, 17, 58, '+00:00')
|
45
|
+
|
46
|
+
base = ::AWS::SES::Base.new(
|
47
|
+
access_key_id: aws_access_key_id,
|
48
|
+
secret_access_key: aws_secret_access_key
|
49
|
+
)
|
50
|
+
|
51
|
+
assert_equal 'AWS3-HTTPS AWSAccessKeyId=fake_aws_key_id, Algorithm=HmacSHA256, Signature=eHh/cPIJJUc1+RMCueAi50EPlYxkZNXMrxtGxjkBD1w=', base.get_aws_auth_param(timestamp.httpdate, aws_secret_access_key)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_ses_authorization_header_v4
|
55
|
+
aws_access_key_id = 'fake_aws_key_id'
|
56
|
+
aws_secret_access_key = 'fake_aws_access_key'
|
57
|
+
time = Time.new(2020, 7, 2, 7, 17, 58, '+00:00')
|
58
|
+
::Timecop.freeze(time)
|
59
|
+
|
60
|
+
base = ::AWS::SES::Base.new(
|
61
|
+
server: 'ec2.amazonaws.com',
|
62
|
+
signature_version: 4,
|
63
|
+
access_key_id: aws_access_key_id,
|
64
|
+
secret_access_key: aws_secret_access_key
|
65
|
+
)
|
66
|
+
|
67
|
+
assert_equal 'AWS4-HMAC-SHA256 Credential=fake_aws_key_id/20200702/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-date, Signature=c0465b36efd110b14a1c6dcca3e105085ed2bfb2a3fd3b3586cc459326ab43aa', base.get_aws_auth_param(time.httpdate, aws_secret_access_key, 'DescribeRegions', 4)
|
68
|
+
Timecop.return
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_ses_authorization_header_v4_changed_host
|
72
|
+
aws_access_key_id = 'fake_aws_key_id'
|
73
|
+
aws_secret_access_key = 'fake_aws_access_key'
|
74
|
+
time = Time.new(2020, 7, 2, 7, 17, 58, '+00:00')
|
75
|
+
::Timecop.freeze(time)
|
76
|
+
|
77
|
+
base = ::AWS::SES::Base.new(
|
78
|
+
server: 'email.us-east-1.amazonaws.com',
|
79
|
+
signature_version: 4,
|
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/ec2/aws4_request, SignedHeaders=host;x-amz-date, Signature=b872601457070ab98e7038bdcd4dc1f5eab586ececf9908525474408b0740515', base.get_aws_auth_param(time.httpdate, aws_secret_access_key, 'DescribeRegions', 4)
|
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
|
+
signature_version: 4,
|
97
|
+
access_key_id: aws_access_key_id,
|
98
|
+
secret_access_key: aws_secret_access_key,
|
99
|
+
region: 'eu-west-1'
|
100
|
+
)
|
101
|
+
|
102
|
+
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_param(time.httpdate, aws_secret_access_key, 'DescribeRegions', 4)
|
103
|
+
Timecop.return
|
104
|
+
end
|
40
105
|
end
|
data/test/helper.rb
CHANGED
@@ -24,6 +24,7 @@ require File.dirname(__FILE__) + '/fixtures'
|
|
24
24
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
25
25
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
26
26
|
require 'aws/ses'
|
27
|
+
require 'timecop'
|
27
28
|
|
28
29
|
class Test::Unit::TestCase
|
29
30
|
require 'net/http'
|
@@ -54,3 +55,9 @@ class Test::Unit::TestCase
|
|
54
55
|
Base.new(:access_key_id=>'123', :secret_access_key=>'abc')
|
55
56
|
end
|
56
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
|