rapns_rails_2 3.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.
- checksums.yaml +15 -0
- data/CHANGELOG.md +83 -0
- data/LICENSE +7 -0
- data/README.md +168 -0
- data/bin/rapns +37 -0
- data/config/database.yml +44 -0
- data/lib/generators/rapns_generator.rb +25 -0
- data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
- data/lib/generators/templates/add_app_to_rapns.rb +11 -0
- data/lib/generators/templates/add_gcm.rb +95 -0
- data/lib/generators/templates/create_rapns_apps.rb +16 -0
- data/lib/generators/templates/create_rapns_feedback.rb +15 -0
- data/lib/generators/templates/create_rapns_notifications.rb +26 -0
- data/lib/generators/templates/rapns.rb +87 -0
- data/lib/rapns/TODO +3 -0
- data/lib/rapns/apns/app.rb +25 -0
- data/lib/rapns/apns/binary_notification_validator.rb +12 -0
- data/lib/rapns/apns/device_token_format_validator.rb +12 -0
- data/lib/rapns/apns/feedback.rb +16 -0
- data/lib/rapns/apns/notification.rb +91 -0
- data/lib/rapns/apns_feedback.rb +13 -0
- data/lib/rapns/app.rb +16 -0
- data/lib/rapns/configuration.rb +89 -0
- data/lib/rapns/daemon/apns/app_runner.rb +26 -0
- data/lib/rapns/daemon/apns/certificate_expired_error.rb +20 -0
- data/lib/rapns/daemon/apns/connection.rb +142 -0
- data/lib/rapns/daemon/apns/delivery.rb +64 -0
- data/lib/rapns/daemon/apns/delivery_handler.rb +35 -0
- data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rapns/daemon/apns/feedback_receiver.rb +89 -0
- data/lib/rapns/daemon/app_runner.rb +179 -0
- data/lib/rapns/daemon/batch.rb +112 -0
- data/lib/rapns/daemon/delivery.rb +23 -0
- data/lib/rapns/daemon/delivery_error.rb +19 -0
- data/lib/rapns/daemon/delivery_handler.rb +52 -0
- data/lib/rapns/daemon/delivery_handler_collection.rb +33 -0
- data/lib/rapns/daemon/feeder.rb +65 -0
- data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
- data/lib/rapns/daemon/gcm/delivery.rb +228 -0
- data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
- data/lib/rapns/daemon/interruptible_sleep.rb +65 -0
- data/lib/rapns/daemon/reflectable.rb +13 -0
- data/lib/rapns/daemon/store/active_record/reconnectable.rb +66 -0
- data/lib/rapns/daemon/store/active_record.rb +128 -0
- data/lib/rapns/daemon.rb +129 -0
- data/lib/rapns/deprecatable.rb +23 -0
- data/lib/rapns/deprecation.rb +23 -0
- data/lib/rapns/embed.rb +28 -0
- data/lib/rapns/gcm/app.rb +7 -0
- data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
- data/lib/rapns/gcm/notification.rb +37 -0
- data/lib/rapns/gcm/payload_data_size_validator.rb +13 -0
- data/lib/rapns/gcm/registration_ids_count_validator.rb +13 -0
- data/lib/rapns/logger.rb +76 -0
- data/lib/rapns/multi_json_helper.rb +16 -0
- data/lib/rapns/notification.rb +62 -0
- data/lib/rapns/notifier.rb +35 -0
- data/lib/rapns/push.rb +17 -0
- data/lib/rapns/rails-2-compatibility.rb +34 -0
- data/lib/rapns/reflection.rb +44 -0
- data/lib/rapns/upgraded.rb +31 -0
- data/lib/rapns/version.rb +3 -0
- data/lib/rapns_rails_2.rb +67 -0
- data/lib/tasks/cane.rake +18 -0
- data/lib/tasks/test.rake +38 -0
- data/spec/support/cert_with_password.pem +90 -0
- data/spec/support/cert_without_password.pem +59 -0
- data/spec/support/simplecov_helper.rb +13 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/tmp/.gitkeep +0 -0
- data/spec/unit/apns/app_spec.rb +29 -0
- data/spec/unit/apns/feedback_spec.rb +9 -0
- data/spec/unit/apns/notification_spec.rb +215 -0
- data/spec/unit/apns_feedback_spec.rb +21 -0
- data/spec/unit/app_spec.rb +16 -0
- data/spec/unit/configuration_spec.rb +55 -0
- data/spec/unit/daemon/apns/app_runner_spec.rb +45 -0
- data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
- data/spec/unit/daemon/apns/connection_spec.rb +287 -0
- data/spec/unit/daemon/apns/delivery_handler_spec.rb +59 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +101 -0
- data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +134 -0
- data/spec/unit/daemon/app_runner_shared.rb +83 -0
- data/spec/unit/daemon/app_runner_spec.rb +170 -0
- data/spec/unit/daemon/batch_spec.rb +219 -0
- data/spec/unit/daemon/delivery_error_spec.rb +13 -0
- data/spec/unit/daemon/delivery_handler_collection_spec.rb +37 -0
- data/spec/unit/daemon/delivery_handler_shared.rb +45 -0
- data/spec/unit/daemon/feeder_spec.rb +81 -0
- data/spec/unit/daemon/gcm/app_runner_spec.rb +19 -0
- data/spec/unit/daemon/gcm/delivery_handler_spec.rb +44 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +289 -0
- data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
- data/spec/unit/daemon/reflectable_spec.rb +27 -0
- data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
- data/spec/unit/daemon/store/active_record_spec.rb +281 -0
- data/spec/unit/daemon_spec.rb +157 -0
- data/spec/unit/deprecatable_spec.rb +32 -0
- data/spec/unit/deprecation_spec.rb +15 -0
- data/spec/unit/embed_spec.rb +50 -0
- data/spec/unit/gcm/app_spec.rb +4 -0
- data/spec/unit/gcm/notification_spec.rb +52 -0
- data/spec/unit/logger_spec.rb +180 -0
- data/spec/unit/notification_shared.rb +45 -0
- data/spec/unit/notification_spec.rb +4 -0
- data/spec/unit/notifier_spec.rb +32 -0
- data/spec/unit/push_spec.rb +44 -0
- data/spec/unit/rapns_spec.rb +9 -0
- data/spec/unit/reflection_spec.rb +30 -0
- data/spec/unit/upgraded_spec.rb +40 -0
- data/spec/unit_spec_helper.rb +137 -0
- metadata +232 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
require File.expand_path("spec/unit_spec_helper")
|
|
2
|
+
|
|
3
|
+
describe Rapns::Daemon::Apns::Connection do
|
|
4
|
+
let(:rsa_key) { double }
|
|
5
|
+
let(:certificate) { double }
|
|
6
|
+
let(:password) { double }
|
|
7
|
+
let(:x509_certificate) { OpenSSL::X509::Certificate.new(TEST_CERT) }
|
|
8
|
+
let(:ssl_context) { double(:key= => nil, :cert= => nil, :cert => x509_certificate) }
|
|
9
|
+
let(:host) { 'gateway.push.apple.com' }
|
|
10
|
+
let(:port) { '2195' }
|
|
11
|
+
let(:tcp_socket) { double(:setsockopt => nil, :close => nil) }
|
|
12
|
+
let(:ssl_socket) { double(:sync= => nil, :connect => nil, :close => nil, :write => nil, :flush => nil) }
|
|
13
|
+
let(:logger) { double(:info => nil, :error => nil, :warn => nil) }
|
|
14
|
+
let(:app) { double(:name => 'Connection 0', :certificate => certificate, :password => password)}
|
|
15
|
+
let(:connection) { Rapns::Daemon::Apns::Connection.new(app, host, port) }
|
|
16
|
+
|
|
17
|
+
before do
|
|
18
|
+
OpenSSL::SSL::SSLContext.stub(:new => ssl_context)
|
|
19
|
+
OpenSSL::PKey::RSA.stub(:new => rsa_key)
|
|
20
|
+
OpenSSL::X509::Certificate.stub(:new => x509_certificate)
|
|
21
|
+
TCPSocket.stub(:new => tcp_socket)
|
|
22
|
+
OpenSSL::SSL::SSLSocket.stub(:new => ssl_socket)
|
|
23
|
+
Rapns.stub(:logger => logger)
|
|
24
|
+
connection.stub(:reflect)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "reads the number of bytes from the SSL socket" do
|
|
28
|
+
ssl_socket.should_receive(:read).with(123)
|
|
29
|
+
connection.connect
|
|
30
|
+
connection.read(123)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "selects on the SSL socket until the given timeout" do
|
|
34
|
+
IO.should_receive(:select).with([ssl_socket], nil, nil, 10)
|
|
35
|
+
connection.connect
|
|
36
|
+
connection.select(10)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "when setting up the SSL context" do
|
|
40
|
+
it "sets the key on the context" do
|
|
41
|
+
OpenSSL::PKey::RSA.should_receive(:new).with(certificate, password).and_return(rsa_key)
|
|
42
|
+
ssl_context.should_receive(:key=).with(rsa_key)
|
|
43
|
+
connection.connect
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "sets the cert on the context" do
|
|
47
|
+
OpenSSL::X509::Certificate.should_receive(:new).with(certificate).and_return(x509_certificate)
|
|
48
|
+
ssl_context.should_receive(:cert=).with(x509_certificate)
|
|
49
|
+
connection.connect
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "when connecting the socket" do
|
|
54
|
+
it "creates a TCP socket using the configured host and port" do
|
|
55
|
+
TCPSocket.should_receive(:new).with(host, port).and_return(tcp_socket)
|
|
56
|
+
connection.connect
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "creates a new SSL socket using the TCP socket and SSL context" do
|
|
60
|
+
OpenSSL::SSL::SSLSocket.should_receive(:new).with(tcp_socket, ssl_context).and_return(ssl_socket)
|
|
61
|
+
connection.connect
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "sets the sync option on the SSL socket" do
|
|
65
|
+
ssl_socket.should_receive(:sync=).with(true)
|
|
66
|
+
connection.connect
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "connects the SSL socket" do
|
|
70
|
+
ssl_socket.should_receive(:connect)
|
|
71
|
+
connection.connect
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "sets the socket option TCP_NODELAY" do
|
|
75
|
+
tcp_socket.should_receive(:setsockopt).with(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
76
|
+
connection.connect
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "sets the socket option SO_KEEPALIVE" do
|
|
80
|
+
tcp_socket.should_receive(:setsockopt).with(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
|
|
81
|
+
connection.connect
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe 'certificate expiry' do
|
|
85
|
+
it 'reflects if the certificate will expire soon' do
|
|
86
|
+
cert = OpenSSL::X509::Certificate.new(app.certificate)
|
|
87
|
+
connection.should_receive(:reflect).with(:apns_certificate_will_expire, app, cert.not_after)
|
|
88
|
+
Timecop.freeze(cert.not_after.to_datetime - 3.days) { connection.connect }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'logs that the certificate will expire soon' do
|
|
92
|
+
cert = OpenSSL::X509::Certificate.new(app.certificate)
|
|
93
|
+
logger.should_receive(:warn).with("[#{app.name}] Certificate will expire at 2022-09-07 03:18:32 UTC.")
|
|
94
|
+
Timecop.freeze(cert.not_after.to_datetime - 3.days) { connection.connect }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'does not reflect if the certificate will not expire soon' do
|
|
98
|
+
cert = OpenSSL::X509::Certificate.new(app.certificate)
|
|
99
|
+
connection.should_not_receive(:reflect).with(:apns_certificate_will_expire, app, kind_of(Time))
|
|
100
|
+
Timecop.freeze(cert.not_after.to_datetime - 2.months) { connection.connect }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'logs that the certificate has expired' do
|
|
104
|
+
cert = OpenSSL::X509::Certificate.new(app.certificate)
|
|
105
|
+
logger.should_receive(:error).with("[#{app.name}] Certificate expired at 2022-09-07 03:18:32 UTC.")
|
|
106
|
+
Timecop.freeze(cert.not_after.to_datetime + 1.day) { connection.connect rescue Rapns::Apns::CertificateExpiredError }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'raises an error if the certificate has expired' do
|
|
110
|
+
cert = OpenSSL::X509::Certificate.new(app.certificate)
|
|
111
|
+
Timecop.freeze(cert.not_after.to_datetime + 1.day) do
|
|
112
|
+
expect { connection.connect }.to raise_error(Rapns::Apns::CertificateExpiredError)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe "when shuting down the connection" do
|
|
119
|
+
it "closes the TCP socket" do
|
|
120
|
+
connection.connect
|
|
121
|
+
tcp_socket.should_receive(:close)
|
|
122
|
+
connection.close
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "does not attempt to close the TCP socket if it is not connected" do
|
|
126
|
+
connection.connect
|
|
127
|
+
tcp_socket.should_not_receive(:close)
|
|
128
|
+
connection.instance_variable_set("@tcp_socket", nil)
|
|
129
|
+
connection.close
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "closes the SSL socket" do
|
|
133
|
+
connection.connect
|
|
134
|
+
ssl_socket.should_receive(:close)
|
|
135
|
+
connection.close
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "does not attempt to close the SSL socket if it is not connected" do
|
|
139
|
+
connection.connect
|
|
140
|
+
ssl_socket.should_not_receive(:close)
|
|
141
|
+
connection.instance_variable_set("@ssl_socket", nil)
|
|
142
|
+
connection.close
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "ignores IOError when the socket is already closed" do
|
|
146
|
+
tcp_socket.stub(:close).and_raise(IOError)
|
|
147
|
+
connection.connect
|
|
148
|
+
connection.close
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
shared_examples_for "when the write fails" do
|
|
153
|
+
before do
|
|
154
|
+
connection.stub(:sleep)
|
|
155
|
+
connection.connect
|
|
156
|
+
ssl_socket.stub(:write).and_raise(error_type)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'reflects the connection has been lost' do
|
|
160
|
+
connection.should_receive(:reflect).with(:apns_connection_lost, app, kind_of(error_type))
|
|
161
|
+
begin
|
|
162
|
+
connection.write(nil)
|
|
163
|
+
rescue Rapns::Daemon::Apns::ConnectionError
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "logs that the connection has been lost once only" do
|
|
168
|
+
logger.should_receive(:error).with("[Connection 0] Lost connection to gateway.push.apple.com:2195 (#{error_type.name}), reconnecting...").once
|
|
169
|
+
begin
|
|
170
|
+
connection.write(nil)
|
|
171
|
+
rescue Rapns::Daemon::Apns::ConnectionError
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "retries to make a connection 3 times" do
|
|
176
|
+
connection.should_receive(:reconnect).exactly(3).times
|
|
177
|
+
begin
|
|
178
|
+
connection.write(nil)
|
|
179
|
+
rescue Rapns::Daemon::Apns::ConnectionError
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "raises a ConnectionError after 3 attempts at reconnecting" do
|
|
184
|
+
expect do
|
|
185
|
+
connection.write(nil)
|
|
186
|
+
end.to raise_error(Rapns::Daemon::Apns::ConnectionError, "Connection 0 tried 3 times to reconnect but failed (#{error_type.name}).")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "sleeps 1 second before retrying the connection" do
|
|
190
|
+
connection.should_receive(:sleep).with(1)
|
|
191
|
+
begin
|
|
192
|
+
connection.write(nil)
|
|
193
|
+
rescue Rapns::Daemon::Apns::ConnectionError
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
describe "when write raises an Errno::EPIPE" do
|
|
199
|
+
it_should_behave_like "when the write fails"
|
|
200
|
+
|
|
201
|
+
def error_type
|
|
202
|
+
Errno::EPIPE
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
describe "when write raises an Errno::ETIMEDOUT" do
|
|
207
|
+
it_should_behave_like "when the write fails"
|
|
208
|
+
|
|
209
|
+
def error_type
|
|
210
|
+
Errno::ETIMEDOUT
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
describe "when write raises an OpenSSL::SSL::SSLError" do
|
|
215
|
+
it_should_behave_like "when the write fails"
|
|
216
|
+
|
|
217
|
+
def error_type
|
|
218
|
+
OpenSSL::SSL::SSLError
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
describe "when write raises an IOError" do
|
|
223
|
+
it_should_behave_like "when the write fails"
|
|
224
|
+
|
|
225
|
+
def error_type
|
|
226
|
+
IOError
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
describe "when reconnecting" do
|
|
231
|
+
before { connection.connect }
|
|
232
|
+
|
|
233
|
+
it 'closes the socket' do
|
|
234
|
+
connection.should_receive(:close)
|
|
235
|
+
connection.send(:reconnect)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'connects the socket' do
|
|
239
|
+
connection.should_receive(:connect_socket)
|
|
240
|
+
connection.send(:reconnect)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
describe "when sending a notification" do
|
|
245
|
+
before { connection.connect }
|
|
246
|
+
|
|
247
|
+
it "writes the data to the SSL socket" do
|
|
248
|
+
ssl_socket.should_receive(:write).with("blah")
|
|
249
|
+
connection.write("blah")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it "flushes the SSL socket" do
|
|
253
|
+
ssl_socket.should_receive(:flush)
|
|
254
|
+
connection.write("blah")
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
describe 'idle period' do
|
|
259
|
+
before { connection.connect }
|
|
260
|
+
|
|
261
|
+
it 'reconnects if the connection has been idle for more than the defined period' do
|
|
262
|
+
Rapns::Daemon::Apns::Connection.stub(:idle_period => 0.1)
|
|
263
|
+
sleep 0.2
|
|
264
|
+
connection.should_receive(:reconnect)
|
|
265
|
+
connection.write('blah')
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'resets the last write time' do
|
|
269
|
+
now = Time.now
|
|
270
|
+
Time.stub(:now => now)
|
|
271
|
+
connection.write('blah')
|
|
272
|
+
connection.last_write.should == now
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'does not reconnect if the connection has not been idle for more than the defined period' do
|
|
276
|
+
connection.should_not_receive(:reconnect)
|
|
277
|
+
connection.write('blah')
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it 'logs the the connection is idle' do
|
|
281
|
+
Rapns::Daemon::Apns::Connection.stub(:idle_period => 0.1)
|
|
282
|
+
sleep 0.2
|
|
283
|
+
Rapns.logger.should_receive(:info).with('[Connection 0] Idle period exceeded, reconnecting...')
|
|
284
|
+
connection.write('blah')
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require File.expand_path("spec/unit_spec_helper")
|
|
2
|
+
require File.dirname(__FILE__) + '/../delivery_handler_shared.rb'
|
|
3
|
+
|
|
4
|
+
describe Rapns::Daemon::Apns::DeliveryHandler do
|
|
5
|
+
it_should_behave_like 'an DeliveryHandler subclass'
|
|
6
|
+
|
|
7
|
+
let(:host) { 'gateway.push.apple.com' }
|
|
8
|
+
let(:port) { 2195 }
|
|
9
|
+
let(:certificate) { double }
|
|
10
|
+
let(:password) { double }
|
|
11
|
+
let(:app) { double(:password => password, :certificate => certificate, :name => 'MyApp', :environment => 'production')}
|
|
12
|
+
let(:delivery_handler) { Rapns::Daemon::Apns::DeliveryHandler.new(app) }
|
|
13
|
+
let(:connection) { double(:connection, :select => false, :write => nil, :reconnect => nil, :close => nil, :connect => nil) }
|
|
14
|
+
let(:notification) { double(:notification) }
|
|
15
|
+
let(:batch) { double(:batch, :notification_processed => nil) }
|
|
16
|
+
let(:http) { double(:http, :shutdown => nil)}
|
|
17
|
+
let(:queue) { Queue.new }
|
|
18
|
+
let(:delivery) { double(:perform => nil) }
|
|
19
|
+
|
|
20
|
+
before do
|
|
21
|
+
Rapns::Daemon::Apns::Connection.stub(:new => connection)
|
|
22
|
+
Rapns::Daemon::Apns::Delivery.stub(:new => delivery)
|
|
23
|
+
delivery_handler.queue = queue
|
|
24
|
+
queue.push([notification, batch])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run_delivery_handler
|
|
28
|
+
delivery_handler.start
|
|
29
|
+
delivery_handler.stop
|
|
30
|
+
delivery_handler.wakeup
|
|
31
|
+
delivery_handler.wait
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "instantiates a new connection" do
|
|
35
|
+
Rapns::Daemon::Apns::Connection.should_receive(:new).with(app, host, port)
|
|
36
|
+
run_delivery_handler
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "connects the socket" do
|
|
40
|
+
connection.should_receive(:connect)
|
|
41
|
+
run_delivery_handler
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'performs delivery of an notification' do
|
|
45
|
+
Rapns::Daemon::Apns::Delivery.should_receive(:new).with(app, connection, notification, batch).and_return(delivery)
|
|
46
|
+
delivery.should_receive(:perform)
|
|
47
|
+
run_delivery_handler
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'closes the connection stopped' do
|
|
51
|
+
connection.should_receive(:close)
|
|
52
|
+
run_delivery_handler
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'does not attempt to close the connection if the connection was not established' do
|
|
56
|
+
connection.should_not_receive(:close)
|
|
57
|
+
delivery_handler.stop
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require File.expand_path("spec/unit_spec_helper")
|
|
2
|
+
|
|
3
|
+
describe Rapns::Daemon::Apns::Delivery do
|
|
4
|
+
let(:app) { double(:name => 'MyApp') }
|
|
5
|
+
let(:notification) { double(:id => 123).as_null_object }
|
|
6
|
+
let(:batch) { double(:mark_failed => nil, :mark_delivered => nil) }
|
|
7
|
+
let(:logger) { double(:error => nil, :info => nil) }
|
|
8
|
+
let(:config) { double(:check_for_errors => true) }
|
|
9
|
+
let(:connection) { double(:select => false, :write => nil, :reconnect => nil, :close => nil, :connect => nil) }
|
|
10
|
+
let(:delivery) { Rapns::Daemon::Apns::Delivery.new(app, connection, notification, batch) }
|
|
11
|
+
|
|
12
|
+
def perform
|
|
13
|
+
begin
|
|
14
|
+
delivery.perform
|
|
15
|
+
rescue Rapns::DeliveryError, Rapns::Apns::DisconnectionError
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
before do
|
|
20
|
+
Rapns.stub(:config => config, :logger => logger)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "sends the binary version of the notification" do
|
|
24
|
+
notification.stub(:to_binary => "hi mom")
|
|
25
|
+
connection.should_receive(:write).with("hi mom")
|
|
26
|
+
perform
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "logs the notification delivery" do
|
|
30
|
+
notification.stub(:id => 666, :device_token => 'abc123')
|
|
31
|
+
logger.should_receive(:info).with("[MyApp] 666 sent to abc123")
|
|
32
|
+
perform
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "marks the notification as delivered" do
|
|
36
|
+
batch.should_receive(:mark_delivered).with(notification)
|
|
37
|
+
perform
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'does not check for errors if check_for_errors config option is false' do
|
|
41
|
+
config.stub(:check_for_errors => false)
|
|
42
|
+
delivery.should_not_receive(:check_for_error)
|
|
43
|
+
perform
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe "when delivery fails" do
|
|
47
|
+
before { connection.stub(:select => true, :read => [8, 4, 69].pack("ccN")) }
|
|
48
|
+
|
|
49
|
+
it "marks the notification as failed" do
|
|
50
|
+
batch.should_receive(:mark_failed).with(notification, 4, "Missing payload")
|
|
51
|
+
perform
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "logs the delivery error" do
|
|
55
|
+
# checking for the doublebed error doesn't work in jruby, but checking
|
|
56
|
+
# for the exception by class does.
|
|
57
|
+
|
|
58
|
+
#error = Rapns::DeliveryError.new(4, 12, "Missing payload")
|
|
59
|
+
#Rapns::DeliveryError.stub(:new => error)
|
|
60
|
+
#expect { delivery.perform }.to raise_error(error)
|
|
61
|
+
|
|
62
|
+
expect { delivery.perform }.to raise_error(Rapns::DeliveryError)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "reads 6 bytes from the socket" do
|
|
66
|
+
connection.should_receive(:read).with(6).and_return(nil)
|
|
67
|
+
perform
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "does not attempt to read from the socket if the socket was not selected for reading after the timeout" do
|
|
71
|
+
connection.stub(:select => nil)
|
|
72
|
+
connection.should_not_receive(:read)
|
|
73
|
+
perform
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "reconnects the socket" do
|
|
77
|
+
connection.should_receive(:reconnect)
|
|
78
|
+
perform
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "logs that the connection is being reconnected" do
|
|
82
|
+
Rapns.logger.should_receive(:error).with("[MyApp] Error received, reconnecting...")
|
|
83
|
+
perform
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
context "when the APNs disconnects without returning an error" do
|
|
87
|
+
before do
|
|
88
|
+
connection.stub(:read => nil)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'raises a DisconnectError error if the connection is closed without an error being returned' do
|
|
92
|
+
expect { delivery.perform }.to raise_error(Rapns::Apns::DisconnectionError)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'marks the notification as failed' do
|
|
96
|
+
batch.should_receive(:mark_failed).with(notification, nil, "APNs disconnected without returning an error.")
|
|
97
|
+
perform
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require File.expand_path("spec/unit_spec_helper")
|
|
2
|
+
|
|
3
|
+
describe Rapns::Apns::DisconnectionError do
|
|
4
|
+
let(:error) { Rapns::Apns::DisconnectionError.new }
|
|
5
|
+
|
|
6
|
+
it 'returns a nil error code' do
|
|
7
|
+
error.code.should be_nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'contains an error description' do
|
|
11
|
+
error.description
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'returns a message' do
|
|
15
|
+
error.message
|
|
16
|
+
error.to_s
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require File.expand_path("spec/unit_spec_helper")
|
|
2
|
+
|
|
3
|
+
describe Rapns::Daemon::Apns::FeedbackReceiver, 'check_for_feedback' do
|
|
4
|
+
|
|
5
|
+
let(:host) { 'feedback.push.apple.com' }
|
|
6
|
+
let(:port) { 2196 }
|
|
7
|
+
let(:poll) { 60 }
|
|
8
|
+
let(:certificate) { double }
|
|
9
|
+
let(:password) { double }
|
|
10
|
+
let(:app) { double(:name => 'my_app', :password => password, :certificate => certificate, :environment => 'production') }
|
|
11
|
+
let(:connection) { double(:connect => nil, :read => nil, :close => nil) }
|
|
12
|
+
let(:logger) { double(:error => nil, :info => nil) }
|
|
13
|
+
let(:receiver) { Rapns::Daemon::Apns::FeedbackReceiver.new(app, poll) }
|
|
14
|
+
let(:feedback) { double }
|
|
15
|
+
|
|
16
|
+
before do
|
|
17
|
+
receiver.stub(:interruptible_sleep)
|
|
18
|
+
Rapns.stub(:logger => logger)
|
|
19
|
+
Rapns::Daemon::Apns::Connection.stub(:new => connection)
|
|
20
|
+
receiver.instance_variable_set("@stop", false)
|
|
21
|
+
Rapns::Daemon.stub(:store => double(:create_apns_feedback => feedback))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def double_connection_read_with_tuple
|
|
25
|
+
connection.unstub(:read)
|
|
26
|
+
|
|
27
|
+
def connection.read(bytes)
|
|
28
|
+
if !@called
|
|
29
|
+
@called = true
|
|
30
|
+
"N\xE3\x84\r\x00 \x83OxfU\xEB\x9F\x84aJ\x05\xAD}\x00\xAF1\xE5\xCF\xE9:\xC3\xEA\a\x8F\x1D\xA4M*N\xB0\xCE\x17"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'instantiates a new connection' do
|
|
36
|
+
Rapns::Daemon::Apns::Connection.should_receive(:new).with(app, host, port)
|
|
37
|
+
receiver.check_for_feedback
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'connects to the feeback service' do
|
|
41
|
+
connection.should_receive(:connect)
|
|
42
|
+
receiver.check_for_feedback
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'closes the connection' do
|
|
46
|
+
connection.should_receive(:close)
|
|
47
|
+
receiver.check_for_feedback
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'reads from the connection' do
|
|
51
|
+
connection.should_receive(:read).with(38)
|
|
52
|
+
receiver.check_for_feedback
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'logs the feedback' do
|
|
56
|
+
double_connection_read_with_tuple
|
|
57
|
+
Rapns.logger.should_receive(:info).with("[my_app] [FeedbackReceiver] Delivery failed at 2011-12-10 16:08:45 UTC for 834f786655eb9f84614a05ad7d00af31e5cfe93ac3ea078f1da44d2a4eb0ce17.")
|
|
58
|
+
receiver.check_for_feedback
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'creates the feedback' do
|
|
62
|
+
Rapns::Daemon.store.should_receive(:create_apns_feedback).with(Time.at(1323533325), '834f786655eb9f84614a05ad7d00af31e5cfe93ac3ea078f1da44d2a4eb0ce17', app)
|
|
63
|
+
double_connection_read_with_tuple
|
|
64
|
+
receiver.check_for_feedback
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'logs errors' do
|
|
68
|
+
error = StandardError.new('bork!')
|
|
69
|
+
connection.stub(:read).and_raise(error)
|
|
70
|
+
Rapns.logger.should_receive(:error).with(error)
|
|
71
|
+
receiver.check_for_feedback
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'sleeps for the feedback poll period' do
|
|
75
|
+
receiver.stub(:check_for_feedback)
|
|
76
|
+
sleeper = double(Rapns::Daemon::InterruptibleSleep, :sleep => false)
|
|
77
|
+
sleeper.should_receive(:sleep).with(60).at_least(:once)
|
|
78
|
+
receiver.stub :interruptible_sleep => sleeper
|
|
79
|
+
Thread.stub(:new).and_yield
|
|
80
|
+
receiver.stub(:loop).and_yield
|
|
81
|
+
receiver.start
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'checks for feedback when started' do
|
|
85
|
+
sleeper = double(Rapns::Daemon::InterruptibleSleep, :sleep => false)
|
|
86
|
+
receiver.stub :interruptible_sleep => sleeper
|
|
87
|
+
receiver.should_receive(:check_for_feedback).at_least(:once)
|
|
88
|
+
Thread.stub(:new).and_yield
|
|
89
|
+
receiver.stub(:loop).and_yield
|
|
90
|
+
receiver.start
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'interrupts sleep when stopped' do
|
|
94
|
+
sleeper = double(Rapns::Daemon::InterruptibleSleep, :sleep => false)
|
|
95
|
+
receiver.stub :interruptible_sleep => sleeper
|
|
96
|
+
receiver.stub(:check_for_feedback)
|
|
97
|
+
sleeper.should_receive(:interrupt_sleep)
|
|
98
|
+
receiver.stop
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'reflects feedback was received' do
|
|
102
|
+
double_connection_read_with_tuple
|
|
103
|
+
receiver.should_receive(:reflect).with(:apns_feedback, feedback)
|
|
104
|
+
receiver.check_for_feedback
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'calls the apns_feedback_callback when feedback is received and the callback is set' do
|
|
108
|
+
double_connection_read_with_tuple
|
|
109
|
+
Rapns.config.apns_feedback_callback = Proc.new {}
|
|
110
|
+
Rapns.config.apns_feedback_callback.should_receive(:call).with(feedback)
|
|
111
|
+
receiver.check_for_feedback
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'catches exceptions in the apns_feedback_callback' do
|
|
115
|
+
error = StandardError.new('bork!')
|
|
116
|
+
double_connection_read_with_tuple
|
|
117
|
+
callback = Proc.new { raise error }
|
|
118
|
+
Rapns::Deprecation.muted do
|
|
119
|
+
Rapns.config.on_apns_feedback &callback
|
|
120
|
+
end
|
|
121
|
+
lambda { receiver.check_for_feedback }.should_not raise_error
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'logs an exception from the apns_feedback_callback' do
|
|
125
|
+
error = StandardError.new('bork!')
|
|
126
|
+
double_connection_read_with_tuple
|
|
127
|
+
callback = Proc.new { raise error }
|
|
128
|
+
Rapns.logger.should_receive(:error).with(error)
|
|
129
|
+
Rapns::Deprecation.muted do
|
|
130
|
+
Rapns.config.on_apns_feedback &callback
|
|
131
|
+
end
|
|
132
|
+
receiver.check_for_feedback
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
shared_examples_for "an AppRunner subclass" do
|
|
2
|
+
let(:queue) { double(:push => nil) }
|
|
3
|
+
|
|
4
|
+
before { Queue.stub(:new => queue) }
|
|
5
|
+
after { Rapns::Daemon::AppRunner.runners.clear }
|
|
6
|
+
|
|
7
|
+
describe 'start' do
|
|
8
|
+
it 'starts a delivery handler for each connection' do
|
|
9
|
+
handler.should_receive(:start)
|
|
10
|
+
runner.start
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'adds the delivery handler to the collection' do
|
|
14
|
+
handler_collection.should_receive(:push).with(handler)
|
|
15
|
+
runner.start
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'assigns the queue to the handler' do
|
|
19
|
+
handler.should_receive(:queue=).with(queue)
|
|
20
|
+
runner.start
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe 'enqueue' do
|
|
25
|
+
let(:notification) { double }
|
|
26
|
+
let(:batch) { double(:notifications => [notification]) }
|
|
27
|
+
|
|
28
|
+
it 'enqueues the batch' do
|
|
29
|
+
queue.should_receive(:push).with([notification, batch])
|
|
30
|
+
runner.enqueue(batch)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'reflects the notification has been enqueued' do
|
|
34
|
+
runner.should_receive(:reflect).with(:notification_enqueued, notification)
|
|
35
|
+
runner.enqueue(batch)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe 'stop' do
|
|
40
|
+
before { runner.start }
|
|
41
|
+
|
|
42
|
+
it 'stops the delivery handlers' do
|
|
43
|
+
handler_collection.should_receive(:stop)
|
|
44
|
+
runner.stop
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe 'idle?' do
|
|
49
|
+
it 'is idle if all notifications have been processed' do
|
|
50
|
+
runner.batch = double(:complete? => true)
|
|
51
|
+
runner.idle?.should be_true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'is idle if the runner has no associated batch' do
|
|
55
|
+
runner.batch = nil
|
|
56
|
+
runner.idle?.should be_true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'is not idle if not all notifications have been processed' do
|
|
60
|
+
runner.batch = double(:complete? => false)
|
|
61
|
+
runner.idle?.should be_false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe 'sync' do
|
|
66
|
+
before { runner.start }
|
|
67
|
+
|
|
68
|
+
it 'reduces the number of handlers if needed' do
|
|
69
|
+
handler_collection.should_receive(:pop)
|
|
70
|
+
new_app = app_class.new
|
|
71
|
+
new_app.stub(:connections => app.connections - 1)
|
|
72
|
+
runner.sync(new_app)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'increases the number of handlers if needed' do
|
|
76
|
+
runner.should_receive(:start_handler).and_return(handler)
|
|
77
|
+
handler_collection.should_receive(:push).with(handler)
|
|
78
|
+
new_app = app_class.new
|
|
79
|
+
new_app.stub(:connections => app.connections + 1)
|
|
80
|
+
runner.sync(new_app)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|