aws-ses 0.4.3 → 0.7.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/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
|