mailgun-ruby 1.4.1 → 1.4.3

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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -8
  3. data/.rubocop.yml +64 -4
  4. data/Gemfile +3 -1
  5. data/README.md +1 -1
  6. data/Rakefile +5 -8
  7. data/docs/AnalyticsTags.md +63 -0
  8. data/lib/mailgun/address.rb +5 -5
  9. data/lib/mailgun/chains.rb +2 -3
  10. data/lib/mailgun/client.rb +56 -56
  11. data/lib/mailgun/domains/domains.rb +11 -10
  12. data/lib/mailgun/events/events.rb +4 -3
  13. data/lib/mailgun/exceptions/exceptions.rb +12 -15
  14. data/lib/mailgun/helpers/api_version_checker.rb +6 -1
  15. data/lib/mailgun/lists/opt_in_handler.rb +6 -10
  16. data/lib/mailgun/logs/logs.rb +4 -2
  17. data/lib/mailgun/messages/batch_message.rb +10 -10
  18. data/lib/mailgun/messages/message_builder.rb +40 -56
  19. data/lib/mailgun/metrics/metrics.rb +12 -6
  20. data/lib/mailgun/response.rb +12 -10
  21. data/lib/mailgun/subaccounts/subaccounts.rb +13 -8
  22. data/lib/mailgun/suppressions.rb +36 -43
  23. data/lib/mailgun/tags/analytics_tags.rb +37 -2
  24. data/lib/mailgun/tags/tags.rb +29 -19
  25. data/lib/mailgun/templates/templates.rb +40 -29
  26. data/lib/mailgun/version.rb +3 -1
  27. data/lib/mailgun/webhooks/webhooks.rb +22 -19
  28. data/lib/mailgun-ruby.rb +2 -0
  29. data/lib/mailgun.rb +4 -4
  30. data/lib/railgun/attachment.rb +12 -19
  31. data/lib/railgun/errors.rb +2 -3
  32. data/lib/railgun/mailer.rb +37 -41
  33. data/lib/railgun/railtie.rb +2 -0
  34. data/lib/railgun.rb +2 -0
  35. data/mailgun.gemspec +15 -11
  36. data/spec/integration/analytics_tags_spec.rb +54 -0
  37. data/spec/integration/bounces_spec.rb +12 -11
  38. data/spec/integration/campaign_spec.rb +20 -18
  39. data/spec/integration/complaints_spec.rb +8 -6
  40. data/spec/integration/domains_spec.rb +12 -18
  41. data/spec/integration/email_validation_spec.rb +35 -34
  42. data/spec/integration/events_spec.rb +8 -8
  43. data/spec/integration/list_members_spec.rb +27 -26
  44. data/spec/integration/list_spec.rb +22 -21
  45. data/spec/integration/logs_spec.rb +49 -47
  46. data/spec/integration/mailer_spec.rb +7 -3
  47. data/spec/integration/mailgun_spec.rb +85 -92
  48. data/spec/integration/metrics_spec.rb +137 -131
  49. data/spec/integration/routes_spec.rb +41 -40
  50. data/spec/integration/stats_spec.rb +4 -2
  51. data/spec/integration/subaccounts_spec.rb +9 -10
  52. data/spec/integration/suppressions_spec.rb +222 -44
  53. data/spec/integration/templates_spec.rb +14 -12
  54. data/spec/integration/unsubscribes_spec.rb +8 -6
  55. data/spec/integration/webhook_spec.rb +18 -12
  56. data/spec/spec_helper.rb +15 -8
  57. data/spec/unit/client_spec.rb +424 -0
  58. data/spec/unit/connection/test_client.rb +108 -55
  59. data/spec/unit/events/events_spec.rb +48 -29
  60. data/spec/unit/exceptions/exceptions_spec.rb +8 -7
  61. data/spec/unit/helpers/api_version_checker_spec.rb +206 -0
  62. data/spec/unit/lists/opt_in_handler_spec.rb +11 -7
  63. data/spec/unit/mailgun_spec.rb +71 -68
  64. data/spec/unit/messages/batch_message_spec.rb +37 -36
  65. data/spec/unit/messages/message_builder_spec.rb +170 -169
  66. data/spec/unit/railgun/content_type_spec.rb +31 -30
  67. data/spec/unit/railgun/mailer_spec.rb +62 -59
  68. data/spec/unit/response_spec.rb +225 -0
  69. data/vcr_cassettes/For_the_suppressions_handling_class/creates_a_single_bounce.yml +55 -0
  70. data/vcr_cassettes/analytics_tags.yml +187 -0
  71. data/vcr_cassettes/suppressions.yml +1053 -170
  72. metadata +95 -29
  73. data/.rubocop_todo.yml +0 -22
@@ -1,50 +1,51 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'mailgun'
3
5
  require 'railgun'
4
6
 
5
7
  describe 'extract_body' do
6
-
7
- let(:text_mail_option) {
8
+ let(:text_mail_option) do
8
9
  {
9
- from: 'bob@example.com',
10
- to: 'sally@example.com',
11
- subject: 'RAILGUN TEST SAMPLE',
12
- body: text_content,
13
- content_type: 'text/plain',
10
+ from: 'bob@example.com',
11
+ to: 'sally@example.com',
12
+ subject: 'RAILGUN TEST SAMPLE',
13
+ body: text_content,
14
+ content_type: 'text/plain'
14
15
  }
15
- }
16
+ end
16
17
  let(:text_content) { '[TEST] Hello, world.' }
17
18
 
18
- let(:html_mail_option) {
19
+ let(:html_mail_option) do
19
20
  {
20
- from: 'bob@example.com',
21
- to: 'sally@example.com',
22
- subject: 'RAILGUN TEST SAMPLE',
23
- body: html_content,
24
- content_type: 'text/html',
21
+ from: 'bob@example.com',
22
+ to: 'sally@example.com',
23
+ subject: 'RAILGUN TEST SAMPLE',
24
+ body: html_content,
25
+ content_type: 'text/html'
25
26
  }
26
- }
27
+ end
27
28
  let(:html_content) { '<h3> [TEST] </h3> <br/> Hello, world!' }
28
29
 
29
- let(:amp_mail_option) {
30
+ let(:amp_mail_option) do
30
31
  {
31
- from: 'bob@example.com',
32
- to: 'sally@example.com',
33
- subject: 'RAILGUN TEST SAMPLE',
34
- body: amp_content,
35
- content_type: 'text/x-amp-html',
32
+ from: 'bob@example.com',
33
+ to: 'sally@example.com',
34
+ subject: 'RAILGUN TEST SAMPLE',
35
+ body: amp_content,
36
+ content_type: 'text/x-amp-html'
36
37
  }
37
- }
38
+ end
38
39
  let(:amp_content) { '<h3> [TEST] </h3> <br/> Hello from AMP!' }
39
40
 
40
41
  context 'with <Content-Type: text/plain>' do
41
42
  let(:sample_mail) { Mail.new(text_mail_option) }
42
43
 
43
- it 'should return body text' do
44
+ it 'returns body text' do
44
45
  expect(Railgun.extract_body_text(sample_mail)).to eq(text_content)
45
46
  end
46
47
 
47
- it 'should not return body html' do
48
+ it 'does not return body html' do
48
49
  expect(Railgun.extract_body_html(sample_mail)).to be_nil
49
50
  end
50
51
  end
@@ -52,11 +53,11 @@ describe 'extract_body' do
52
53
  context 'with <Content-Type: text/html>' do
53
54
  let(:sample_mail) { Mail.new(html_mail_option) }
54
55
 
55
- it 'should not return body text' do
56
+ it 'does not return body text' do
56
57
  expect(Railgun.extract_body_text(sample_mail)).to be_nil
57
58
  end
58
59
 
59
- it 'should return body html' do
60
+ it 'returns body html' do
60
61
  expect(Railgun.extract_body_html(sample_mail)).to eq(html_content)
61
62
  end
62
63
  end
@@ -67,21 +68,21 @@ describe 'extract_body' do
67
68
  let(:amp_mail) { Mail.new(amp_mail_option) }
68
69
 
69
70
  before do
70
- @sample_mail = Mail::Part.new(content_type: "multipart/alternative")
71
+ @sample_mail = Mail::Part.new(content_type: 'multipart/alternative')
71
72
  @sample_mail.add_part text_mail
72
73
  @sample_mail.add_part amp_mail
73
74
  @sample_mail.add_part html_mail
74
75
  end
75
76
 
76
- it 'should return body text' do
77
+ it 'returns body text' do
77
78
  expect(Railgun.extract_body_text(@sample_mail)).to eq(text_content)
78
79
  end
79
80
 
80
- it 'should return body html' do
81
+ it 'returns body html' do
81
82
  expect(Railgun.extract_body_html(@sample_mail)).to eq(html_content)
82
83
  end
83
84
 
84
- it 'should return AMP html' do
85
+ it 'returns AMP html' do
85
86
  expect(Railgun.extract_amp_html(@sample_mail)).to eq(amp_content)
86
87
  end
87
88
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'logger'
3
5
  require 'spec_helper'
@@ -15,42 +17,40 @@ class UnitTestMailer < ActionMailer::Base
15
17
  def plain_message(address, subject, headers)
16
18
  headers(headers)
17
19
  mail(to: address, subject: subject) do |format|
18
- format.text { render plain: "Test!" }
19
- format.html { render html: "<p>Test!</p>".html_safe }
20
+ format.text { render plain: 'Test!' }
21
+ format.html { render html: '<p>Test!</p>'.html_safe }
20
22
  end
21
23
  end
22
24
 
23
25
  def message_with_attachment(address, subject)
24
26
  attachments['info.txt'] = {
25
- :content => File.read('docs/railgun/Overview.md'),
26
- :mime_type => 'text/plain',
27
+ content: File.read('docs/railgun/Overview.md'),
28
+ mime_type: 'text/plain'
27
29
  }
28
30
  mail(to: address, subject: subject) do |format|
29
- format.text { render plain: "Test!" }
30
- format.html { render html: "<p>Test!</p>".html_safe }
31
+ format.text { render plain: 'Test!' }
32
+ format.html { render html: '<p>Test!</p>'.html_safe }
31
33
  end
32
34
  end
33
35
 
34
36
  def message_with_template(address, subject, template_name)
35
37
  mail(to: address, subject: subject, template: template_name) do |format|
36
- format.text { render plain: "Test!" }
38
+ format.text { render plain: 'Test!' }
37
39
  end
38
40
  end
39
41
 
40
42
  def message_with_domain(address, subject, domain)
41
43
  mail(to: address, subject: subject, domain: domain) do |format|
42
- format.text { render plain: "Test!" }
44
+ format.text { render plain: 'Test!' }
43
45
  end
44
46
  end
45
-
46
47
  end
47
48
 
48
49
  describe 'Railgun::Mailer' do
49
-
50
50
  it 'has a mailgun_client property which returns a Mailgun::Client' do
51
51
  config = {
52
- api_key: {},
53
- domain: {}
52
+ api_key: {},
53
+ domain: {}
54
54
  }
55
55
  @mailer_obj = Railgun::Mailer.new(config)
56
56
 
@@ -60,18 +60,18 @@ describe 'Railgun::Mailer' do
60
60
  context 'when config does not have api_key or domain' do
61
61
  it 'raises configuration error' do
62
62
  config = {
63
- api_key: {}
63
+ api_key: {}
64
64
  }
65
65
 
66
- expect { Railgun::Mailer.new(config) }.to raise_error(Railgun::ConfigurationError)
66
+ expect { Railgun::Mailer.new(config) }.to raise_error(Railgun::ConfigurationError)
67
67
  end
68
68
  end
69
69
 
70
70
  context 'when fake_message_send is present in config' do
71
71
  it 'enables test mode' do
72
72
  config = {
73
- api_key: {},
74
- domain: {},
73
+ api_key: {},
74
+ domain: {},
75
75
  fake_message_send: true
76
76
  }
77
77
  client_double = double(Mailgun::Client)
@@ -100,7 +100,7 @@ describe 'Railgun::Mailer' do
100
100
  it 'adds options to message body' do
101
101
  message = UnitTestMailer.plain_message('test@example.org', '', {})
102
102
  message.mailgun_options ||= {
103
- 'tracking-opens' => 'true',
103
+ 'tracking-opens' => 'true'
104
104
  }
105
105
 
106
106
  body = Railgun.transform_for_mailgun(message)
@@ -124,14 +124,14 @@ describe 'Railgun::Mailer' do
124
124
  it 'adds variables to message body' do
125
125
  message = UnitTestMailer.plain_message('test@example.org', '', {})
126
126
  message.mailgun_variables ||= {
127
- 'user' => {:id => '1', :name => 'tstark'},
127
+ 'user' => { id: '1', name: 'tstark' }
128
128
  }
129
129
 
130
130
  body = Railgun.transform_for_mailgun(message)
131
131
 
132
132
  expect(body).to include('v:user')
133
133
 
134
- var_body = JSON.load(body['v:user'])
134
+ var_body = JSON.parse(body['v:user'])
135
135
  expect(var_body).to include('id')
136
136
  expect(var_body).to include('name')
137
137
  expect(var_body['id']).to eq('1')
@@ -141,7 +141,7 @@ describe 'Railgun::Mailer' do
141
141
  it 'adds headers to message body' do
142
142
  message = UnitTestMailer.plain_message('test@example.org', '', {})
143
143
  message.mailgun_headers ||= {
144
- 'x-unit-test' => 'true',
144
+ 'x-unit-test' => 'true'
145
145
  }
146
146
 
147
147
  body = Railgun.transform_for_mailgun(message)
@@ -152,8 +152,8 @@ describe 'Railgun::Mailer' do
152
152
 
153
153
  it 'adds headers to message body from mailer' do
154
154
  message = UnitTestMailer.plain_message('test@example.org', '', {
155
- 'x-unit-test-2' => 'true',
156
- })
155
+ 'x-unit-test-2' => 'true'
156
+ })
157
157
 
158
158
  body = Railgun.transform_for_mailgun(message)
159
159
 
@@ -162,21 +162,24 @@ describe 'Railgun::Mailer' do
162
162
  end
163
163
 
164
164
  it 'properly handles headers that are passed as separate POST params' do
165
- message = UnitTestMailer.plain_message('test@example.org', 'Test!', {
166
- # `From`, `To`, and `Subject` are set on the envelope, so they should be ignored as headers
167
- 'From' => 'units@example.net',
168
- 'To' => 'user@example.com',
169
- 'Subject' => 'This should disappear',
170
- # If `Bcc` or `Cc` are set as headers, they should be carried over as POST params, not headers
171
- 'Bcc' => ['list@example.org'],
172
- 'Cc' => ['admin@example.com'],
173
- # This is an arbitrary header and should be carried over properly
174
- 'X-Source' => 'unit tests',
175
- })
165
+ message = UnitTestMailer.plain_message(
166
+ 'test@example.org', 'Test!',
167
+ {
168
+ # `From`, `To`, and `Subject` are set on the envelope, so they should be ignored as headers
169
+ 'From' => 'units@example.net',
170
+ 'To' => 'user@example.com',
171
+ 'Subject' => 'This should disappear',
172
+ # If `Bcc` or `Cc` are set as headers, they should be carried over as POST params, not headers
173
+ 'Bcc' => ['list@example.org'],
174
+ 'Cc' => ['admin@example.com'],
175
+ # This is an arbitrary header and should be carried over properly
176
+ 'X-Source' => 'unit tests'
177
+ }
178
+ )
176
179
 
177
180
  body = Railgun.transform_for_mailgun(message)
178
181
 
179
- ['From', 'To', 'Subject'].each do |header|
182
+ %w[From To Subject].each do |header|
180
183
  expect(body).not_to include("h:#{header}")
181
184
  end
182
185
 
@@ -202,20 +205,20 @@ describe 'Railgun::Mailer' do
202
205
  }
203
206
  end
204
207
  body = Railgun.transform_for_mailgun(message)
205
- expect(body["v:my-data"]).to be_kind_of(String)
206
- expect(body["v:my-data"].to_s).to eq('{"key":"value"}')
208
+ expect(body['v:my-data']).to be_a(String)
209
+ expect(body['v:my-data'].to_s).to eq('{"key":"value"}')
207
210
  end
208
211
 
209
212
  it 'accepts a hash and appends as data to the message.' do
210
213
  message = UnitTestMailer.plain_message('test@example.org', '', {}).tap do |message|
211
214
  message.mailgun_variables = {
212
- 'my-data' => {'key' => 'value'}
215
+ 'my-data' => { 'key' => 'value' }
213
216
  }
214
217
  end
215
218
  body = Railgun.transform_for_mailgun(message)
216
219
 
217
- expect(body["v:my-data"]).to be_kind_of(String)
218
- expect(body["v:my-data"].to_s).to eq('{"key":"value"}')
220
+ expect(body['v:my-data']).to be_a(String)
221
+ expect(body['v:my-data'].to_s).to eq('{"key":"value"}')
219
222
  end
220
223
 
221
224
  it 'accepts string values' do
@@ -226,8 +229,8 @@ describe 'Railgun::Mailer' do
226
229
  end
227
230
  body = Railgun.transform_for_mailgun(message)
228
231
 
229
- expect(body["v:my-data"]).to be_kind_of(String)
230
- expect(body["v:my-data"].to_s).to eq('String Value.')
232
+ expect(body['v:my-data']).to be_a(String)
233
+ expect(body['v:my-data'].to_s).to eq('String Value.')
231
234
  end
232
235
  end
233
236
 
@@ -251,13 +254,13 @@ describe 'Railgun::Mailer' do
251
254
 
252
255
  it 'ignores `reply-to` in headers' do
253
256
  message = UnitTestMailer.plain_message('test@example.org', '', {
254
- 'reply-to' => 'user@example.com',
255
- })
257
+ 'reply-to' => 'user@example.com'
258
+ })
256
259
  message.mailgun_headers = {
257
- 'Reply-To' => 'administrator@example.org',
260
+ 'Reply-To' => 'administrator@example.org'
258
261
  }
259
- message.headers({'REPLY-TO' => 'admin@example.net'})
260
- message.reply_to = "dude@example.com.au"
262
+ message.headers({ 'REPLY-TO' => 'admin@example.net' })
263
+ message.reply_to = 'dude@example.com.au'
261
264
 
262
265
  body = Railgun.transform_for_mailgun(message)
263
266
  expect(body).to include('h:reply-to')
@@ -267,12 +270,12 @@ describe 'Railgun::Mailer' do
267
270
 
268
271
  it 'ignores `mime-version` in headers' do
269
272
  message = UnitTestMailer.plain_message('test@example.org', '', {
270
- 'mime-version' => '1.0',
271
- })
273
+ 'mime-version' => '1.0'
274
+ })
272
275
  message.mailgun_headers = {
273
- 'Mime-Version' => '1.1',
276
+ 'Mime-Version' => '1.1'
274
277
  }
275
- message.headers({'MIME-VERSION' => '1.2'})
278
+ message.headers({ 'MIME-VERSION' => '1.2' })
276
279
 
277
280
  body = Railgun.transform_for_mailgun(message)
278
281
  expect(body).not_to include('h:mime-version')
@@ -280,32 +283,32 @@ describe 'Railgun::Mailer' do
280
283
 
281
284
  it 'treats `headers()` names as case-insensitve' do
282
285
  message = UnitTestMailer.plain_message('test@example.org', '', {
283
- 'X-BIG-VALUE' => 1,
284
- })
286
+ 'X-BIG-VALUE' => 1
287
+ })
285
288
 
286
289
  body = Railgun.transform_for_mailgun(message)
287
290
  expect(body).to include('h:x-big-value')
288
- expect(body['h:x-big-value']).to eq("1")
291
+ expect(body['h:x-big-value']).to eq('1')
289
292
  end
290
293
 
291
294
  it 'treats `mailgun_headers` names as case-insensitive' do
292
295
  message = UnitTestMailer.plain_message('test@example.org', '', {})
293
296
  message.mailgun_headers = {
294
- 'X-BIG-VALUE' => 1,
297
+ 'X-BIG-VALUE' => 1
295
298
  }
296
299
 
297
300
  body = Railgun.transform_for_mailgun(message)
298
301
  expect(body).to include('h:x-big-value')
299
- expect(body['h:x-big-value']).to eq("1")
302
+ expect(body['h:x-big-value']).to eq('1')
300
303
  end
301
304
 
302
305
  it 'handles multi-value, mixed case headers correctly' do
303
306
  message = UnitTestMailer.plain_message('test@example.org', '', {})
304
307
  message.headers({
305
- 'x-neat-header' => 'foo',
306
- 'X-Neat-Header' => 'bar',
307
- 'X-NEAT-HEADER' => 'zoop',
308
- })
308
+ 'x-neat-header' => 'foo',
309
+ 'X-Neat-Header' => 'bar',
310
+ 'X-NEAT-HEADER' => 'zoop'
311
+ })
309
312
 
310
313
  body = Railgun.transform_for_mailgun(message)
311
314
  expect(body).to include('h:x-neat-header')
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'mailgun/response'
7
+ require 'mailgun/exceptions/exceptions'
8
+
9
+ describe Mailgun::Response do
10
+ subject(:response) { described_class.new(http_response) }
11
+
12
+ let(:json_body) { '{"id":"<message-id@example.com>","message":"Queued. Thank you."}' }
13
+ let(:status200) { 200 }
14
+
15
+ # Minimal double that mimics a RestClient::Response
16
+ let(:http_response) do
17
+ double('http_response', body: json_body, status: status200)
18
+ end
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # .new
22
+ # ---------------------------------------------------------------------------
23
+ describe '#initialize' do
24
+ it 'sets body from the underlying response' do
25
+ expect(response.body).to eq(json_body)
26
+ end
27
+
28
+ it 'sets status from the underlying response' do
29
+ expect(response.status).to eq(status200)
30
+ end
31
+
32
+ it 'aliases code to status' do
33
+ expect(response.code).to eq(status200)
34
+ end
35
+ end
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # .from_hash
39
+ # ---------------------------------------------------------------------------
40
+ describe '.from_hash' do
41
+ subject(:from_hash_response) do
42
+ described_class.from_hash(body: json_body, status: status200)
43
+ end
44
+
45
+ it 'returns a Response instance' do
46
+ expect(from_hash_response).to be_a(described_class)
47
+ end
48
+
49
+ it 'sets body from the hash' do
50
+ expect(from_hash_response.body).to eq(json_body)
51
+ end
52
+
53
+ it 'sets status from the hash' do
54
+ expect(from_hash_response.status).to eq(status200)
55
+ end
56
+
57
+ it 'aliases code to status' do
58
+ expect(from_hash_response.code).to eq(status200)
59
+ end
60
+ end
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # #to_h
64
+ # ---------------------------------------------------------------------------
65
+ describe '#to_h' do
66
+ it 'returns a Hash' do
67
+ expect(response.to_h).to be_a(Hash)
68
+ end
69
+
70
+ it 'correctly parses the JSON body' do
71
+ result = response.to_h
72
+ expect(result['id']).to eq('<message-id@example.com>')
73
+ expect(result['message']).to eq('Queued. Thank you.')
74
+ end
75
+
76
+ it 'does not mutate the body attribute' do
77
+ expect { response.to_h }.not_to(change(response, :body))
78
+ end
79
+
80
+ context 'when the body is invalid JSON' do
81
+ let(:http_response) { double('http_response', body: 'not-json', status: 200) }
82
+
83
+ it 'raises a Mailgun::ParseError' do
84
+ expect { response.to_h }.to raise_error(Mailgun::ParseError)
85
+ end
86
+ end
87
+ end
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # #to_h!
91
+ # ---------------------------------------------------------------------------
92
+ describe '#to_h!' do
93
+ it 'returns a Hash' do
94
+ expect(response.to_h!).to be_a(Hash)
95
+ end
96
+
97
+ it 'correctly parses the JSON body' do
98
+ result = response.to_h!
99
+ expect(result['message']).to eq('Queued. Thank you.')
100
+ end
101
+
102
+ it 'replaces body with the parsed Hash' do
103
+ response.to_h!
104
+ expect(response.body).to be_a(Hash)
105
+ expect(response.body['id']).to eq('<message-id@example.com>')
106
+ end
107
+
108
+ context 'when the body is invalid JSON' do
109
+ let(:http_response) { double('http_response', body: 'not-json', status: 200) }
110
+
111
+ it 'raises a Mailgun::ParseError' do
112
+ expect { response.to_h! }.to raise_error(Mailgun::ParseError)
113
+ end
114
+ end
115
+ end
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # #to_yaml
119
+ # ---------------------------------------------------------------------------
120
+ describe '#to_yaml' do
121
+ it 'returns a String' do
122
+ expect(response.to_yaml).to be_a(String)
123
+ end
124
+
125
+ it 'returns valid YAML that round-trips back to the expected hash' do
126
+ parsed = YAML.safe_load(response.to_yaml)
127
+ expect(parsed['id']).to eq('<message-id@example.com>')
128
+ expect(parsed['message']).to eq('Queued. Thank you.')
129
+ end
130
+
131
+ it 'does not mutate the body attribute' do
132
+ expect { response.to_yaml }.not_to(change(response, :body))
133
+ end
134
+
135
+ context 'when the body is invalid JSON' do
136
+ let(:http_response) { double('http_response', body: 'not-json', status: 200) }
137
+
138
+ it 'raises a Mailgun::ParseError' do
139
+ expect { response.to_yaml }.to raise_error(Mailgun::ParseError)
140
+ end
141
+ end
142
+ end
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # #to_yaml!
146
+ # ---------------------------------------------------------------------------
147
+ describe '#to_yaml!' do
148
+ it 'returns a String' do
149
+ expect(response.to_yaml!).to be_a(String)
150
+ end
151
+
152
+ it 'returns valid YAML that round-trips back to the expected hash' do
153
+ yaml_str = response.to_yaml!
154
+ parsed = YAML.safe_load(yaml_str)
155
+ expect(parsed['message']).to eq('Queued. Thank you.')
156
+ end
157
+
158
+ it 'replaces body with the YAML string' do
159
+ response.to_yaml!
160
+ expect(response.body).to be_a(String)
161
+ expect(response.body).to include('Queued. Thank you.')
162
+ end
163
+
164
+ context 'when the body is invalid JSON' do
165
+ let(:http_response) { double('http_response', body: 'not-json', status: 200) }
166
+
167
+ it 'raises a Mailgun::ParseError' do
168
+ expect { response.to_yaml! }.to raise_error(Mailgun::ParseError)
169
+ end
170
+ end
171
+ end
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # #success?
175
+ # ---------------------------------------------------------------------------
176
+ describe '#success?' do
177
+ context 'with a 2xx status code' do
178
+ [200, 201, 204, 299].each do |code|
179
+ it "returns true for status #{code}" do
180
+ allow(http_response).to receive(:status).and_return(code)
181
+ expect(described_class.new(http_response).success?).to be(true)
182
+ end
183
+ end
184
+ end
185
+
186
+ context 'with a non-2xx status code' do
187
+ [400, 401, 403, 404, 422, 500, 503].each do |code|
188
+ it "returns false for status #{code}" do
189
+ allow(http_response).to receive(:status).and_return(code)
190
+ expect(described_class.new(http_response).success?).to be(false)
191
+ end
192
+ end
193
+ end
194
+
195
+ it 'returns false for status 199 (just below 2xx range)' do
196
+ allow(http_response).to receive(:status).and_return(199)
197
+ expect(described_class.new(http_response).success?).to be(false)
198
+ end
199
+
200
+ it 'returns false for status 300 (just above 2xx range)' do
201
+ allow(http_response).to receive(:status).and_return(300)
202
+ expect(described_class.new(http_response).success?).to be(false)
203
+ end
204
+ end
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # attr_accessor sanity checks
208
+ # ---------------------------------------------------------------------------
209
+ describe 'attribute accessors' do
210
+ it 'allows body to be overwritten' do
211
+ response.body = 'new body'
212
+ expect(response.body).to eq('new body')
213
+ end
214
+
215
+ it 'allows status to be overwritten' do
216
+ response.status = 404
217
+ expect(response.status).to eq(404)
218
+ end
219
+
220
+ it 'allows code to be overwritten' do
221
+ response.code = 500
222
+ expect(response.code).to eq(500)
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,55 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: https://api.mailgun.net/bounces
6
+ body:
7
+ encoding: UTF-8
8
+ string: address=test5%40example.info&code=500&error=integration+testing
9
+ headers:
10
+ User-Agent:
11
+ - mailgun-sdk-ruby/1.4.2
12
+ Accept:
13
+ - "*/*"
14
+ Authorization:
15
+ - Basic XXX
16
+ Content-Type:
17
+ - application/x-www-form-urlencoded
18
+ Accept-Encoding:
19
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
20
+ response:
21
+ status:
22
+ code: 200
23
+ message: OK
24
+ headers:
25
+ Access-Control-Allow-Credentials:
26
+ - 'true'
27
+ Access-Control-Allow-Origin:
28
+ - "*"
29
+ Cache-Control:
30
+ - no-store
31
+ Content-Length:
32
+ - '89'
33
+ Content-Type:
34
+ - application/json; charset=utf-8
35
+ Date:
36
+ - Wed, 25 Mar 2026 18:09:17 GMT
37
+ Strict-Transport-Security:
38
+ - max-age=63072000; includeSubDomains
39
+ X-Mailgun-Key-Id:
40
+ - XXX
41
+ X-Request-Limit:
42
+ - '2500'
43
+ X-Request-Remaining:
44
+ - '2498'
45
+ X-Request-Reset:
46
+ - '1774462171770'
47
+ X-Xss-Protection:
48
+ - 1; mode=block
49
+ body:
50
+ encoding: UTF-8
51
+ string: '{"message":"Address has been added to the bounces table","address":"test5@example.info"}
52
+
53
+ '
54
+ recorded_at: Wed, 25 Mar 2026 18:09:17 GMT
55
+ recorded_with: VCR 6.4.0