postmark 1.10.0 → 1.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.rdoc +7 -0
- data/CONTRIBUTING.md +18 -0
- data/Gemfile +6 -5
- data/LICENSE +1 -1
- data/README.md +62 -9
- data/VERSION +1 -1
- data/gemfiles/Gemfile.legacy +4 -3
- data/lib/postmark.rb +1 -18
- data/lib/postmark/api_client.rb +32 -1
- data/lib/postmark/client.rb +8 -4
- data/lib/postmark/error.rb +117 -0
- data/lib/postmark/http_client.rb +7 -25
- data/lib/postmark/mail_message_converter.rb +1 -1
- data/lib/postmark/version.rb +1 -1
- data/spec/integration/account_api_client_spec.rb +2 -2
- data/spec/integration/api_client_messages_spec.rb +3 -3
- data/spec/spec_helper.rb +4 -1
- data/spec/support/custom_matchers.rb +30 -0
- data/spec/unit/postmark/account_api_client_spec.rb +22 -22
- data/spec/unit/postmark/api_client_spec.rb +164 -54
- data/spec/unit/postmark/bounce_spec.rb +10 -10
- data/spec/unit/postmark/error_spec.rb +218 -0
- data/spec/unit/postmark/handlers/mail_spec.rb +5 -5
- data/spec/unit/postmark/http_client_spec.rb +4 -5
- data/spec/unit/postmark/mail_message_converter_spec.rb +6 -0
- data/spec/unit/postmark/message_extensions/mail_spec.rb +6 -6
- data/spec/unit/postmark_spec.rb +5 -5
- metadata +33 -4
@@ -43,11 +43,11 @@ describe Postmark::Bounce do
|
|
43
43
|
end
|
44
44
|
|
45
45
|
it 'allows to activate the bounce' do
|
46
|
-
subject.can_activate?.should
|
46
|
+
subject.can_activate?.should be true
|
47
47
|
end
|
48
48
|
|
49
49
|
it 'has an available dump' do
|
50
|
-
subject.dump_available?.should
|
50
|
+
subject.dump_available?.should be true
|
51
51
|
end
|
52
52
|
|
53
53
|
its(:type) { should eq bounce_data[:type] }
|
@@ -69,11 +69,11 @@ describe Postmark::Bounce do
|
|
69
69
|
end
|
70
70
|
|
71
71
|
it 'allows to activate the bounce' do
|
72
|
-
subject.can_activate?.should
|
72
|
+
subject.can_activate?.should be true
|
73
73
|
end
|
74
74
|
|
75
75
|
it 'has an available dump' do
|
76
|
-
subject.dump_available?.should
|
76
|
+
subject.dump_available?.should be true
|
77
77
|
end
|
78
78
|
|
79
79
|
its(:type) { should eq bounce_data[:type] }
|
@@ -93,7 +93,7 @@ describe Postmark::Bounce do
|
|
93
93
|
let(:api_client) { Postmark.api_client }
|
94
94
|
|
95
95
|
it "calls #dump_bounce on shared api_client instance" do
|
96
|
-
Postmark.api_client.
|
96
|
+
expect(Postmark.api_client).to receive(:dump_bounce).with(bounce.id) { response }
|
97
97
|
bounce.dump.should == bounce_body
|
98
98
|
end
|
99
99
|
|
@@ -104,7 +104,7 @@ describe Postmark::Bounce do
|
|
104
104
|
let(:api_client) { Postmark.api_client }
|
105
105
|
|
106
106
|
it "calls #activate_bounce on shared api_client instance" do
|
107
|
-
api_client.
|
107
|
+
expect(api_client).to receive(:activate_bounce).with(bounce.id) { bounce_data }
|
108
108
|
bounce.activate.should be_a Postmark::Bounce
|
109
109
|
end
|
110
110
|
|
@@ -114,7 +114,7 @@ describe Postmark::Bounce do
|
|
114
114
|
let(:api_client) { Postmark.api_client }
|
115
115
|
|
116
116
|
it "calls #get_bounce on shared api_client instance" do
|
117
|
-
api_client.
|
117
|
+
expect(api_client).to receive(:get_bounce).with(42) { bounce_data }
|
118
118
|
Postmark::Bounce.find(42).should be_a Postmark::Bounce
|
119
119
|
end
|
120
120
|
end
|
@@ -125,8 +125,8 @@ describe Postmark::Bounce do
|
|
125
125
|
let(:api_client) { Postmark.api_client }
|
126
126
|
|
127
127
|
it "calls #get_bounces on shared api_client instance" do
|
128
|
-
api_client.
|
129
|
-
Postmark::Bounce.all.should
|
128
|
+
expect(api_client).to receive(:get_bounces) { response }
|
129
|
+
Postmark::Bounce.all.count.should eq(3)
|
130
130
|
end
|
131
131
|
|
132
132
|
end
|
@@ -137,7 +137,7 @@ describe Postmark::Bounce do
|
|
137
137
|
let(:tags) { ["tag1", "tag2"] }
|
138
138
|
|
139
139
|
it "calls #get_bounced_tags on shared api_client instance" do
|
140
|
-
api_client.
|
140
|
+
expect(api_client).to receive(:get_bounced_tags) { tags }
|
141
141
|
Postmark::Bounce.tags.should == tags
|
142
142
|
end
|
143
143
|
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe(Postmark::Error) do
|
4
|
+
it { is_expected.to be_a(StandardError) }
|
5
|
+
end
|
6
|
+
|
7
|
+
describe(Postmark::HttpClientError) do
|
8
|
+
it { is_expected.to be_a(Postmark::Error) }
|
9
|
+
specify { expect(subject.retry?).to be true }
|
10
|
+
end
|
11
|
+
|
12
|
+
describe(Postmark::HttpServerError) do
|
13
|
+
it { is_expected.to be_a(Postmark::Error) }
|
14
|
+
|
15
|
+
describe '.build' do
|
16
|
+
context 'picks an appropriate subclass for code' do
|
17
|
+
subject { Postmark::HttpServerError.build(code, Postmark::Json.encode({})) }
|
18
|
+
|
19
|
+
context '401' do
|
20
|
+
let(:code) { '401' }
|
21
|
+
|
22
|
+
it { is_expected.to be_a(Postmark::InvalidApiKeyError) }
|
23
|
+
its(:status_code) { is_expected.to eq 401 }
|
24
|
+
end
|
25
|
+
|
26
|
+
context '422' do
|
27
|
+
let(:code) { '422' }
|
28
|
+
|
29
|
+
it { is_expected.to be_a(Postmark::ApiInputError) }
|
30
|
+
its(:status_code) { is_expected.to eq 422 }
|
31
|
+
end
|
32
|
+
|
33
|
+
context '500' do
|
34
|
+
let(:code) { '500' }
|
35
|
+
|
36
|
+
it { is_expected.to be_a(Postmark::InternalServerError) }
|
37
|
+
its(:status_code) { is_expected.to eq 500 }
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'others' do
|
41
|
+
let(:code) { '999' }
|
42
|
+
|
43
|
+
it { is_expected.to be_a(Postmark::UnexpectedHttpResponseError) }
|
44
|
+
its(:status_code) { is_expected.to eq code.to_i }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#retry?' do
|
50
|
+
it 'is true for 5XX status codes' do
|
51
|
+
(500...600).each do |code|
|
52
|
+
expect(Postmark::HttpServerError.new(code).retry?).to be true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'is false for other codes except 5XX' do
|
57
|
+
[200, 300, 400].each do |code|
|
58
|
+
expect(Postmark::HttpServerError.new(code).retry?).to be false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#message ' do
|
64
|
+
it 'uses "Message" field on postmark response if available' do
|
65
|
+
data = { 'Message' => 'Postmark error message' }
|
66
|
+
error = Postmark::HttpServerError.new(502, Postmark::Json.encode(data), data)
|
67
|
+
expect(error.message).to eq data['Message']
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'falls back to a message generated from status code' do
|
71
|
+
error = Postmark::HttpServerError.new(502, '<html>')
|
72
|
+
expect(error.message).to match(/The Postmark API responded with HTTP status \d+/)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe(Postmark::ApiInputError) do
|
78
|
+
describe '.build' do
|
79
|
+
context 'picks an appropriate subclass for error code' do
|
80
|
+
let(:response) { { 'ErrorCode' => code } }
|
81
|
+
|
82
|
+
subject do
|
83
|
+
Postmark::ApiInputError.build(Postmark::Json.encode(response), response)
|
84
|
+
end
|
85
|
+
|
86
|
+
shared_examples_for 'api input error' do
|
87
|
+
its(:status_code) { is_expected. to eq 422 }
|
88
|
+
specify { expect(subject.retry?).to be false }
|
89
|
+
it { is_expected.to be_a(Postmark::ApiInputError) }
|
90
|
+
it { is_expected.to be_a(Postmark::HttpServerError) }
|
91
|
+
end
|
92
|
+
|
93
|
+
context '406' do
|
94
|
+
let(:code) { Postmark::ApiInputError::INACTIVE_RECIPIENT }
|
95
|
+
|
96
|
+
it { is_expected.to be_a(Postmark::InactiveRecipientError) }
|
97
|
+
it_behaves_like 'api input error'
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'others' do
|
101
|
+
let(:code) { '9999' }
|
102
|
+
|
103
|
+
it_behaves_like 'api input error'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe(Postmark::TimeoutError) do
|
110
|
+
it { is_expected.to be_a(Postmark::Error) }
|
111
|
+
specify { expect(subject.retry?).to be true }
|
112
|
+
end
|
113
|
+
|
114
|
+
describe(Postmark::UnknownMessageType) do
|
115
|
+
it 'exists for backward compatibility' do
|
116
|
+
is_expected.to be_a(Postmark::Error)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe(Postmark::InvalidApiKeyError) do
|
121
|
+
it { is_expected.to be_a(Postmark::Error) }
|
122
|
+
end
|
123
|
+
|
124
|
+
describe(Postmark::InternalServerError) do
|
125
|
+
it { is_expected.to be_a(Postmark::Error) }
|
126
|
+
end
|
127
|
+
|
128
|
+
describe(Postmark::UnexpectedHttpResponseError) do
|
129
|
+
it { is_expected.to be_a(Postmark::Error) }
|
130
|
+
end
|
131
|
+
|
132
|
+
describe(Postmark::InactiveRecipientError) do
|
133
|
+
describe '.parse_recipients' do
|
134
|
+
let(:recipients) do
|
135
|
+
%w(nothing@wildbit.com noth.ing+2@wildbit.com noth.ing+2-1@wildbit.com)
|
136
|
+
end
|
137
|
+
|
138
|
+
subject { Postmark::InactiveRecipientError.parse_recipients(message) }
|
139
|
+
|
140
|
+
context '1/1 inactive' do
|
141
|
+
let(:message) do
|
142
|
+
'You tried to send to a recipient that has been marked as ' \
|
143
|
+
"inactive.\nFound inactive addresses: #{recipients[0]}.\n" \
|
144
|
+
'Inactive recipients are ones that have generated a hard ' \
|
145
|
+
'bounce or a spam complaint.'
|
146
|
+
end
|
147
|
+
|
148
|
+
it { is_expected.to eq(recipients.take(1)) }
|
149
|
+
end
|
150
|
+
|
151
|
+
context 'i/n inactive, n > 1, i < n' do
|
152
|
+
let(:message) do
|
153
|
+
'Message OK, but will not deliver to these inactive addresses: ' \
|
154
|
+
"#{recipients[0...2].join(', ')}. Inactive recipients are ones that " \
|
155
|
+
'have generated a hard bounce or a spam complaint.'
|
156
|
+
end
|
157
|
+
|
158
|
+
it { is_expected.to eq(recipients.take(2)) }
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'n/n inactive, n > 1' do
|
162
|
+
let(:message) do
|
163
|
+
'You tried to send to recipients that have all been marked as ' \
|
164
|
+
"inactive.\nFound inactive addresses: #{recipients.join(', ')}.\n" \
|
165
|
+
'Inactive recipients are ones that have generated a hard bounce or a spam complaint.'
|
166
|
+
end
|
167
|
+
|
168
|
+
it { is_expected.to eq(recipients) }
|
169
|
+
end
|
170
|
+
|
171
|
+
context 'unknown error format' do
|
172
|
+
let(:message) { recipients.join(', ') }
|
173
|
+
|
174
|
+
it { is_expected.to eq([]) }
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe '.new' do
|
179
|
+
let(:address) { 'user@example.org' }
|
180
|
+
let(:response) { { 'Message' => message } }
|
181
|
+
|
182
|
+
subject do
|
183
|
+
Postmark::InactiveRecipientError.new(
|
184
|
+
Postmark::ApiInputError::INACTIVE_RECIPIENT,
|
185
|
+
Postmark::Json.encode(response),
|
186
|
+
response)
|
187
|
+
end
|
188
|
+
|
189
|
+
let(:message) do
|
190
|
+
'You tried to send to a recipient that has been marked as ' \
|
191
|
+
"inactive.\nFound inactive addresses: #{address}.\n" \
|
192
|
+
'Inactive recipients are ones that have generated a hard ' \
|
193
|
+
'bounce or a spam complaint.'
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'parses recipients from json payload' do
|
197
|
+
expect(subject.recipients).to eq([address])
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
describe(Postmark::DeliveryError) do
|
203
|
+
it 'is an alias to Error for backwards compatibility' do
|
204
|
+
expect(subject.class).to eq(Postmark::Error)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
describe(Postmark::InvalidMessageError) do
|
209
|
+
it 'is an alias to Error for backwards compatibility' do
|
210
|
+
expect(subject.class).to eq(Postmark::ApiInputError)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
describe(Postmark::UnknownError) do
|
215
|
+
it 'is an alias for backwards compatibility' do
|
216
|
+
expect(subject.class).to eq(Postmark::UnexpectedHttpResponseError)
|
217
|
+
end
|
218
|
+
end
|
@@ -15,19 +15,19 @@ describe Mail::Postmark do
|
|
15
15
|
end
|
16
16
|
|
17
17
|
it "wraps Postmark.send_through_postmark" do
|
18
|
-
Postmark::ApiClient.
|
18
|
+
allow_any_instance_of(Postmark::ApiClient).to receive(:deliver_message).with(message)
|
19
19
|
message.delivery_method Mail::Postmark
|
20
20
|
message.deliver
|
21
21
|
end
|
22
22
|
|
23
23
|
it "returns self by default" do
|
24
|
-
Postmark::ApiClient.
|
24
|
+
allow_any_instance_of(Postmark::ApiClient).to receive(:deliver_message).with(message)
|
25
25
|
message.delivery_method Mail::Postmark
|
26
26
|
message.deliver.should eq message
|
27
27
|
end
|
28
28
|
|
29
29
|
it "returns the actual response if :return_response setting is present" do
|
30
|
-
Postmark::ApiClient.
|
30
|
+
allow_any_instance_of(Postmark::ApiClient).to receive(:deliver_message).with(message)
|
31
31
|
message.delivery_method Mail::Postmark, :return_response => true
|
32
32
|
message.deliver.should eq message
|
33
33
|
end
|
@@ -39,13 +39,13 @@ describe Mail::Postmark do
|
|
39
39
|
|
40
40
|
it 'uses provided API token' do
|
41
41
|
message.delivery_method Mail::Postmark, :api_token => 'api-token'
|
42
|
-
Postmark::ApiClient.
|
42
|
+
expect(Postmark::ApiClient).to receive(:new).with('api-token', {}).and_return(double(:deliver_message => true))
|
43
43
|
message.deliver
|
44
44
|
end
|
45
45
|
|
46
46
|
it 'uses API token provided as legacy api_key' do
|
47
47
|
message.delivery_method Mail::Postmark, :api_key => 'api-token'
|
48
|
-
Postmark::ApiClient.
|
48
|
+
expect(Postmark::ApiClient).to receive(:new).with('api-token', {}).and_return(double(:deliver_message => true))
|
49
49
|
message.deliver
|
50
50
|
end
|
51
51
|
end
|
@@ -36,7 +36,7 @@ describe Postmark::HttpClient do
|
|
36
36
|
its(:api_key) { should eq api_token }
|
37
37
|
its(:host) { should eq 'api.postmarkapp.com' }
|
38
38
|
its(:port) { should eq 443 }
|
39
|
-
its(:secure) { should
|
39
|
+
its(:secure) { should be true }
|
40
40
|
its(:path_prefix) { should eq '/' }
|
41
41
|
its(:http_read_timeout) { should eq 15 }
|
42
42
|
its(:http_open_timeout) { should eq 5 }
|
@@ -128,8 +128,7 @@ describe Postmark::HttpClient do
|
|
128
128
|
end
|
129
129
|
|
130
130
|
it "raises a custom error when the request times out" do
|
131
|
-
subject.http.
|
132
|
-
and_raise(Timeout::Error)
|
131
|
+
expect(subject.http).to receive(:post).at_least(:once).and_raise(Timeout::Error)
|
133
132
|
expect { subject.post(target_path) }.to raise_error Postmark::TimeoutError
|
134
133
|
end
|
135
134
|
|
@@ -170,7 +169,7 @@ describe Postmark::HttpClient do
|
|
170
169
|
end
|
171
170
|
|
172
171
|
it "raises a custom error when the request times out" do
|
173
|
-
subject.http.
|
172
|
+
expect(subject.http).to receive(:get).at_least(:once).and_raise(Timeout::Error)
|
174
173
|
expect { subject.get(target_path) }.to raise_error Postmark::TimeoutError
|
175
174
|
end
|
176
175
|
|
@@ -211,7 +210,7 @@ describe Postmark::HttpClient do
|
|
211
210
|
end
|
212
211
|
|
213
212
|
it "raises a custom error when the request times out" do
|
214
|
-
subject.http.
|
213
|
+
expect(subject.http).to receive(:put).at_least(:once).and_raise(Timeout::Error)
|
215
214
|
expect { subject.put(target_path) }.to raise_error Postmark::TimeoutError
|
216
215
|
end
|
217
216
|
|
@@ -311,6 +311,12 @@ describe Postmark::MailMessageConverter do
|
|
311
311
|
"TrackLinks" => 'None'})
|
312
312
|
end
|
313
313
|
|
314
|
+
it 'converts link tracking options when set via header' do
|
315
|
+
msg = mail_html_message
|
316
|
+
msg[:track_links] = :html_and_text
|
317
|
+
expect(subject.new(msg).run).to include('TrackLinks' => 'HtmlAndText')
|
318
|
+
end
|
319
|
+
|
314
320
|
end
|
315
321
|
|
316
322
|
it 'correctly decodes unicode in messages transfered as quoted-printable' do
|
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Mail::Message do
|
4
4
|
before do
|
5
|
-
Kernel.
|
5
|
+
allow(Kernel).to receive(:warn)
|
6
6
|
end
|
7
7
|
|
8
8
|
let(:mail_message) do
|
@@ -147,7 +147,7 @@ describe Mail::Message do
|
|
147
147
|
end
|
148
148
|
|
149
149
|
it "is deprecated" do
|
150
|
-
Kernel.
|
150
|
+
expect(Kernel).to receive(:warn).with(/deprecated/)
|
151
151
|
mail_message.postmark_attachments = attached_hash
|
152
152
|
end
|
153
153
|
end
|
@@ -161,12 +161,12 @@ describe Mail::Message do
|
|
161
161
|
'Content' => ''} }
|
162
162
|
|
163
163
|
before do
|
164
|
-
attached_file.
|
165
|
-
attached_file.
|
164
|
+
allow(attached_file).to receive(:is_a?) { |arg| arg == File ? true : false }
|
165
|
+
allow(attached_file).to receive(:path) { '/tmp/file.jpeg' }
|
166
166
|
end
|
167
167
|
|
168
168
|
it "supports multiple attachment formats" do
|
169
|
-
IO.
|
169
|
+
expect(IO).to receive(:read).with("/tmp/file.jpeg").and_return("")
|
170
170
|
|
171
171
|
mail_message.postmark_attachments = [attached_hash, attached_file]
|
172
172
|
attachments = mail_message.export_attachments
|
@@ -177,7 +177,7 @@ describe Mail::Message do
|
|
177
177
|
|
178
178
|
it "is deprecated" do
|
179
179
|
mail_message.postmark_attachments = attached_hash
|
180
|
-
Kernel.
|
180
|
+
expect(Kernel).to receive(:warn).with(/deprecated/)
|
181
181
|
mail_message.postmark_attachments
|
182
182
|
end
|
183
183
|
end
|
data/spec/unit/postmark_spec.rb
CHANGED
@@ -99,7 +99,7 @@ describe Postmark do
|
|
99
99
|
context "when shared client instance does not exist" do
|
100
100
|
|
101
101
|
it 'creates a new instance of Postmark::ApiClient' do
|
102
|
-
Postmark::ApiClient.
|
102
|
+
allow(Postmark::ApiClient).to receive(:new).
|
103
103
|
with(api_token,
|
104
104
|
:secure => secure,
|
105
105
|
:proxy_host => proxy_host,
|
@@ -127,12 +127,12 @@ describe Postmark do
|
|
127
127
|
end
|
128
128
|
|
129
129
|
it 'delegates the method to the shared api client instance' do
|
130
|
-
api_client.
|
130
|
+
allow(api_client).to receive(:deliver_message).with(message)
|
131
131
|
subject.deliver_message(message)
|
132
132
|
end
|
133
133
|
|
134
134
|
it 'is also accessible as .send_through_postmark' do
|
135
|
-
api_client.
|
135
|
+
allow(api_client).to receive(:deliver_message).with(message)
|
136
136
|
subject.send_through_postmark(message)
|
137
137
|
end
|
138
138
|
end
|
@@ -146,7 +146,7 @@ describe Postmark do
|
|
146
146
|
end
|
147
147
|
|
148
148
|
it 'delegates the method to the shared api client instance' do
|
149
|
-
api_client.
|
149
|
+
allow(api_client).to receive(:deliver_messages).with(message)
|
150
150
|
subject.deliver_messages(message)
|
151
151
|
end
|
152
152
|
end
|
@@ -159,7 +159,7 @@ describe Postmark do
|
|
159
159
|
end
|
160
160
|
|
161
161
|
it 'delegates the method to the shared api client instance' do
|
162
|
-
api_client.
|
162
|
+
allow(api_client).to receive(:delivery_stats)
|
163
163
|
subject.delivery_stats
|
164
164
|
end
|
165
165
|
end
|