rapns 0.1.0

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.
@@ -0,0 +1,76 @@
1
+ require "rapns/daemon/configuration"
2
+ require "rapns/daemon/certificate"
3
+ require "rapns/daemon/delivery_error"
4
+ require "rapns/daemon/pool"
5
+ require "rapns/daemon/connection_pool"
6
+ require "rapns/daemon/connection"
7
+ require "rapns/daemon/delivery_handler"
8
+ require "rapns/daemon/delivery_handler_pool"
9
+ require "rapns/daemon/feeder"
10
+ require "rapns/daemon/logger"
11
+
12
+ module Rapns
13
+ module Daemon
14
+ class << self
15
+ attr_accessor :logger, :configuration, :certificate, :connection_pool, :delivery_queue,
16
+ :delivery_handler_pool, :foreground
17
+ alias_method :foreground?, :foreground
18
+ end
19
+
20
+ def self.start(environment, foreground)
21
+ @foreground = foreground
22
+ setup_signal_hooks
23
+
24
+ self.configuration = Configuration.new(environment, File.join(Rails.root, "config", "rapns", "rapns.yml"))
25
+ configuration.load
26
+
27
+ self.logger = Logger.new(:foreground => foreground, :airbrake_notify => configuration.airbrake_notify)
28
+
29
+ self.certificate = Certificate.new(configuration.certificate)
30
+ certificate.load
31
+
32
+ self.delivery_queue = Queue.new
33
+
34
+ self.delivery_handler_pool = DeliveryHandlerPool.new(configuration.connections)
35
+ delivery_handler_pool.populate
36
+
37
+ self.connection_pool = ConnectionPool.new(configuration.connections)
38
+ connection_pool.populate
39
+
40
+ daemonize unless foreground?
41
+
42
+ Feeder.start
43
+ end
44
+
45
+ protected
46
+
47
+ def self.setup_signal_hooks
48
+ @sigint_received = false
49
+ Signal.trap("SIGINT") do
50
+ exit 1 if @sigint_received
51
+ @sigint_received = true
52
+ shutdown
53
+ end
54
+ end
55
+
56
+ def self.shutdown
57
+ puts "\nShutting down..."
58
+ Rapns::Daemon::Feeder.stop
59
+ Rapns::Daemon.delivery_handler_pool.drain if Rapns::Daemon.delivery_handler_pool
60
+ Rapns::Daemon.connection_pool.drain if Rapns::Daemon.connection_pool
61
+ end
62
+
63
+ def self.daemonize
64
+ exit if pid = fork
65
+ Process.setsid
66
+ exit if pid = fork
67
+
68
+ Dir.chdir '/'
69
+ File.umask 0000
70
+
71
+ STDIN.reopen '/dev/null'
72
+ STDOUT.reopen '/dev/null', 'a'
73
+ STDERR.reopen STDOUT
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,44 @@
1
+ module Rapns
2
+ class Notification < ActiveRecord::Base
3
+ set_table_name "rapns_notifications"
4
+
5
+ validates :device_token, :presence => true, :format => { :with => /^[a-z0-9]{64}$/ }
6
+ validates :badge, :numericality => true, :allow_nil => true
7
+ validates :expiry, :numericality => true, :presence => true
8
+
9
+ validates_with Rapns::BinaryNotificationValidator
10
+
11
+ scope :ready_for_delivery, lambda { where(:delivered => false, :failed => false).merge(where("deliver_after IS NULL") | where("deliver_after < ?", Time.now)) }
12
+
13
+ def device_token=(token)
14
+ write_attribute(:device_token, token.delete(" <>")) if !token.nil?
15
+ end
16
+
17
+ def attributes_for_device=(attrs)
18
+ raise ArgumentError, "attributes_for_device must be a Hash" if !attrs.is_a?(Hash)
19
+ write_attribute(:attributes_for_device, ActiveSupport::JSON.encode(attrs))
20
+ end
21
+
22
+ def attributes_for_device
23
+ ActiveSupport::JSON.decode(read_attribute(:attributes_for_device)) if read_attribute(:attributes_for_device)
24
+ end
25
+
26
+ def as_json
27
+ json = ActiveSupport::OrderedHash.new
28
+ json['aps'] = ActiveSupport::OrderedHash.new
29
+ json['aps']['alert'] = alert if alert
30
+ json['aps']['badge'] = badge if badge
31
+ json['aps']['sound'] = sound if sound
32
+ attributes_for_device.each { |k, v| json[k.to_s] = v.to_s } if attributes_for_device
33
+ json
34
+ end
35
+
36
+ # This method conforms to the enhanced binary format.
37
+ # http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4
38
+ def to_binary(options = {})
39
+ id_for_pack = options[:for_validation] ? 0 : id
40
+ json = as_json.to_json
41
+ [1, id_for_pack, expiry, 0, 32, device_token, 0, json.size, json].pack("cNNccH*cca*")
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module Rapns
2
+ VERSION = "0.1.0"
3
+ end
data/lib/rapns.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "active_record"
2
+
3
+ require "rapns/version"
4
+ require "rapns/binary_notification_validator"
5
+ require "rapns/notification"
@@ -0,0 +1,16 @@
1
+ require "spec_helper"
2
+
3
+ describe Rapns::Daemon::Certificate do
4
+
5
+ it "should raise an error if the .pem file does not exist" do
6
+ cert = Rapns::Daemon::Certificate.new("/tmp/rapns-missing.pem")
7
+ expect { cert.load }.to raise_error(Rapns::CertificateError, "/tmp/rapns-missing.pem does not exist. The certificate location can be configured in config/rapns/rapns.yml.")
8
+ end
9
+
10
+ it "should set the certificate accessor" do
11
+ cert = Rapns::Daemon::Certificate.new("/dir/development.pem")
12
+ cert.stub(:read_certificate).and_return("certificate contents")
13
+ cert.load
14
+ cert.certificate.should == "certificate contents"
15
+ end
16
+ end
@@ -0,0 +1,125 @@
1
+ require "spec_helper"
2
+
3
+ describe Rapns::Daemon::Configuration do
4
+ module Rails
5
+ end
6
+
7
+ before do
8
+ @config = {"port" => 123, "host" => "localhost", "certificate" => "production.pem", "certificate_password" => "abc123", "airbrake_notify" => false, "poll" => 4, "connections" => 6}
9
+ end
10
+
11
+ it "should raise an error if the configuration file does not exist" do
12
+ expect { Rapns::Daemon::Configuration.new("production", "/tmp/rapns-non-existant-file").load }.to raise_error(Rapns::ConfigurationError, "/tmp/rapns-non-existant-file does not exist. Have you run 'rails g rapns'?")
13
+ end
14
+
15
+ it "should raise an error if the environment is not configured" do
16
+ configuration = Rapns::Daemon::Configuration.new("development", "/some/config.yml")
17
+ configuration.stub(:read_config).and_return({"production" => {}})
18
+ expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "Configuration for environment 'development' not defined in /some/config.yml")
19
+ end
20
+
21
+ it "should raise an error if the host is not configured" do
22
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
23
+ configuration.stub(:read_config).and_return({"production" => @config.except("host")})
24
+ expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'host' not defined for environment 'production' in /some/config.yml")
25
+ end
26
+
27
+ it "should raise an error if the port is not configured" do
28
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
29
+ configuration.stub(:read_config).and_return({"production" => @config.except("port")})
30
+ expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'port' not defined for environment 'production' in /some/config.yml")
31
+ end
32
+
33
+ it "should raise an error if the certificate is not configured" do
34
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
35
+ configuration.stub(:read_config).and_return({"production" => @config.except("certificate")})
36
+ expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'certificate' not defined for environment 'production' in /some/config.yml")
37
+ end
38
+
39
+ it "should set the host" do
40
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
41
+ configuration.stub(:read_config).and_return({"production" => @config})
42
+ configuration.load
43
+ configuration.host.should == "localhost"
44
+ end
45
+
46
+ it "should set the port" do
47
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
48
+ configuration.stub(:read_config).and_return({"production" => @config})
49
+ configuration.load
50
+ configuration.port.should == 123
51
+ end
52
+
53
+ it "should set the airbrake notify flag" do
54
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
55
+ configuration.stub(:read_config).and_return({"production" => @config})
56
+ configuration.load
57
+ configuration.airbrake_notify?.should == false
58
+ end
59
+
60
+ it "should default the airbrake notify flag to true if not set" do
61
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
62
+ configuration.stub(:read_config).and_return({"production" => @config.except("airbrake_notify")})
63
+ configuration.load
64
+ configuration.airbrake_notify?.should == true
65
+ end
66
+
67
+ it "should set the poll frequency" do
68
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
69
+ configuration.stub(:read_config).and_return({"production" => @config})
70
+ configuration.load
71
+ configuration.poll.should == 4
72
+ end
73
+
74
+ it "should default the poll frequency to 2 if not set" do
75
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
76
+ configuration.stub(:read_config).and_return({"production" => @config.except("poll")})
77
+ configuration.load
78
+ configuration.poll.should == 2
79
+ end
80
+
81
+ it "should set the number of connections" do
82
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
83
+ configuration.stub(:read_config).and_return({"production" => @config})
84
+ configuration.load
85
+ configuration.connections.should == 6
86
+ end
87
+
88
+ it "should default the number of connections to 3 if not set" do
89
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
90
+ configuration.stub(:read_config).and_return({"production" => @config.except("connections")})
91
+ configuration.load
92
+ configuration.connections.should == 3
93
+ end
94
+
95
+ it "should set the certificate password" do
96
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
97
+ configuration.stub(:read_config).and_return({"production" => @config})
98
+ configuration.load
99
+ configuration.certificate_password.should == "abc123"
100
+ end
101
+
102
+ it "should set the certificate password to a blank string if it is not configured" do
103
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
104
+ configuration.stub(:read_config).and_return({"production" => @config.except("certificate_password")})
105
+ configuration.load
106
+ configuration.certificate_password.should == ""
107
+ end
108
+
109
+ it "should set the certificate, with absolute path" do
110
+ Rails.stub(:root).and_return("/rails_root")
111
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
112
+ configuration.stub(:read_config).and_return({"production" => @config})
113
+ configuration.load
114
+ configuration.certificate.should == "/rails_root/config/rapns/production.pem"
115
+ end
116
+
117
+ it "should keep the absolute path of the certificate if it has one" do
118
+ Rails.stub(:root).and_return("/rails_root")
119
+ @config["certificate"] = "/different_path/to/production.pem"
120
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
121
+ configuration.stub(:read_config).and_return({"production" => @config})
122
+ configuration.load
123
+ configuration.certificate.should == "/different_path/to/production.pem"
124
+ end
125
+ end
@@ -0,0 +1,40 @@
1
+ require "spec_helper"
2
+
3
+ describe Rapns::Daemon::ConnectionPool do
4
+ before do
5
+ @connection = mock("Connection", :connect => nil)
6
+ Rapns::Daemon::Connection.stub(:new).and_return(@connection)
7
+ @pool = Rapns::Daemon::ConnectionPool.new(3)
8
+ end
9
+
10
+ it "should populate the pool" do
11
+ Rapns::Daemon::Connection.should_receive(:new).exactly(3).times
12
+ @pool.populate
13
+ end
14
+
15
+ it "should tell each connection to close when drained" do
16
+ @pool.populate
17
+ @connection.should_receive(:close).exactly(3).times
18
+ @pool.drain
19
+ end
20
+ end
21
+
22
+ describe Rapns::Daemon::ConnectionPool, "when claiming a connection" do
23
+ before do
24
+ @connection = mock("Connection", :connect => nil)
25
+ Rapns::Daemon::Connection.stub(:new).and_return(@connection)
26
+ @pool = Rapns::Daemon::ConnectionPool.new(3)
27
+ end
28
+
29
+ it "should pop the connection from the pool" do
30
+ @pool.instance_variable_get("@queue").should_receive(:pop)
31
+ @pool.claim_connection {}
32
+ end
33
+
34
+ it "shuld push the connection into the pool after use" do
35
+ @pool.instance_variable_get("@queue").stub(:pop).and_return(@connection)
36
+ @pool.instance_variable_get("@queue").should_receive(:push).with(@connection)
37
+ @pool.claim_connection {}
38
+ end
39
+ end
40
+
@@ -0,0 +1,247 @@
1
+ require "spec_helper"
2
+
3
+ describe Rapns::Daemon::Connection, "when setting up the SSL context" do
4
+ before do
5
+ @ssl_context = mock("SSLContext", :key= => nil, :cert= => nil)
6
+ OpenSSL::SSL::SSLContext.should_receive(:new).and_return(@ssl_context)
7
+ @rsa_key = mock("RSA public key")
8
+ OpenSSL::PKey::RSA.stub(:new).and_return(@rsa_key)
9
+ @certificate = mock("Certificate", :certificate => "certificate contents")
10
+ Rapns::Daemon.stub(:certificate).and_return(@certificate)
11
+ @x509_certificate = mock("X509 Certificate")
12
+ OpenSSL::X509::Certificate.stub(:new).and_return(@x509_certificate)
13
+ @connection = Rapns::Daemon::Connection.new("Connection 1")
14
+ @connection.stub(:connect_socket)
15
+ @connection.stub(:setup_at_exit_hook)
16
+ configuration = mock("Configuration", :host => "localhost", :port => 123, :certificate_password => "abc123")
17
+ Rapns::Daemon.stub(:configuration).and_return(configuration)
18
+ end
19
+
20
+ it "should set the key on the context" do
21
+ OpenSSL::PKey::RSA.should_receive(:new).with("certificate contents", "abc123").and_return(@rsa_key)
22
+ @ssl_context.should_receive(:key=).with(@rsa_key)
23
+ @connection.connect
24
+ end
25
+
26
+ it "should set the cert on the context" do
27
+ OpenSSL::X509::Certificate.should_receive(:new).with("certificate contents").and_return(@x509_certificate)
28
+ @ssl_context.should_receive(:cert=).with(@x509_certificate)
29
+ @connection.connect
30
+ end
31
+ end
32
+
33
+ describe Rapns::Daemon::Connection, "when connecting the socket" do
34
+ before do
35
+ @connection = Rapns::Daemon::Connection.new("Connection 1")
36
+ @connection.stub(:setup_at_exit_hook)
37
+ @ssl_context = mock("SSLContext")
38
+ @connection.stub(:setup_ssl_context).and_return(@ssl_context)
39
+ @tcp_socket = mock("TCPSocket", :close => nil)
40
+ TCPSocket.stub(:new).and_return(@tcp_socket)
41
+ Rapns::Daemon::Configuration.stub(:host).and_return("localhost")
42
+ Rapns::Daemon::Configuration.stub(:port).and_return(123)
43
+ @ssl_socket = mock("SSLSocket", :sync= => nil, :connect => nil, :close => nil)
44
+ OpenSSL::SSL::SSLSocket.stub(:new).and_return(@ssl_socket)
45
+ configuration = mock("Configuration", :host => "localhost", :port => 123)
46
+ Rapns::Daemon.stub(:configuration).and_return(configuration)
47
+ @logger = mock("Logger", :info => nil)
48
+ Rapns::Daemon.stub(:logger).and_return(@logger)
49
+ end
50
+
51
+ it "should create a TCP socket using the configured host and port" do
52
+ TCPSocket.should_receive(:new).with("localhost", 123).and_return(@tcp_socket)
53
+ @connection.connect
54
+ end
55
+
56
+ it "should create a new SSL socket using the TCP socket and SSL context" do
57
+ OpenSSL::SSL::SSLSocket.should_receive(:new).with(@tcp_socket, @ssl_context).and_return(@ssl_socket)
58
+ @connection.connect
59
+ end
60
+
61
+ it "should set the sync option on the SSL socket" do
62
+ @ssl_socket.should_receive(:sync=).with(true)
63
+ @connection.connect
64
+ end
65
+
66
+ it "should connect the SSL socket" do
67
+ @ssl_socket.should_receive(:connect)
68
+ @connection.connect
69
+ end
70
+ end
71
+
72
+ describe Rapns::Daemon::Connection, "when shuting down the connection" do
73
+ before do
74
+ @connection = Rapns::Daemon::Connection.new("Connection 1")
75
+ @connection.stub(:setup_ssl_context)
76
+ @ssl_socket = mock("SSLSocket", :close => nil)
77
+ @tcp_socket = mock("TCPSocket", :close => nil)
78
+ @connection.stub(:connect_socket).and_return([@tcp_socket, @ssl_socket])
79
+ end
80
+
81
+ it "should close the TCP socket" do
82
+ @connection.connect
83
+ @tcp_socket.should_receive(:close)
84
+ @connection.close
85
+ end
86
+
87
+ it "should attempt to close the TCP socket if it does not exist" do
88
+ @connection.connect
89
+ @tcp_socket.should_not_receive(:close)
90
+ @connection.instance_variable_set("@tcp_socket", nil)
91
+ @connection.close
92
+ end
93
+
94
+ it "should close the SSL socket" do
95
+ @connection.connect
96
+ @ssl_socket.should_receive(:close)
97
+ @connection.close
98
+ end
99
+
100
+ it "should attempt to close the SSL socket if it does not exist" do
101
+ @connection.connect
102
+ @ssl_socket.should_not_receive(:close)
103
+ @connection.instance_variable_set("@ssl_socket", nil)
104
+ @connection.close
105
+ end
106
+ end
107
+
108
+ describe Rapns::Daemon::Connection, "when the connection is lost" do
109
+ before do
110
+ @connection = Rapns::Daemon::Connection.new("Connection 1")
111
+ @ssl_socket = mock("SSLSocket")
112
+ @connection.instance_variable_set("@ssl_socket", @ssl_socket)
113
+ @connection.stub(:connect_socket).and_return([mock("TCPSocket"), @ssl_socket])
114
+ @ssl_socket.stub(:write).and_raise(Errno::EPIPE)
115
+ @logger = mock("Logger", :warn => nil)
116
+ Rapns::Daemon.stub(:logger).and_return(@logger)
117
+ @connection.stub(:sleep)
118
+ configuration = mock("Configuration", :host => "localhost", :port => 123)
119
+ Rapns::Daemon.stub(:configuration).and_return(configuration)
120
+ end
121
+
122
+ it "should log a warning" do
123
+ Rapns::Daemon.logger.should_receive(:warn).with("[Connection 1] Lost connection to localhost:123, reconnecting...")
124
+ begin
125
+ @connection.write(nil)
126
+ rescue Rapns::Daemon::ConnectionError
127
+ end
128
+ end
129
+
130
+ it "should retry to make a connection 3 times" do
131
+ @connection.should_receive(:connect_socket).exactly(3).times
132
+ begin
133
+ @connection.write(nil)
134
+ rescue Rapns::Daemon::ConnectionError
135
+ end
136
+ end
137
+
138
+ it "should raise a ConnectionError after 3 attempts at reconnecting" do
139
+ expect do
140
+ @connection.write(nil)
141
+ end.to raise_error(Rapns::Daemon::ConnectionError, "Connection 1 tried 3 times to reconnect but failed: #<Errno::EPIPE: Broken pipe>")
142
+ end
143
+
144
+ it "should sleep 1 second before retrying the connection" do
145
+ @connection.should_receive(:sleep).with(1)
146
+ begin
147
+ @connection.write(nil)
148
+ rescue Rapns::Daemon::ConnectionError
149
+ end
150
+ end
151
+ end
152
+
153
+ describe Rapns::Daemon::Connection, "when sending a notification" do
154
+ before do
155
+ @connection = Rapns::Daemon::Connection.new("Connection 1")
156
+ @ssl_socket = mock("SSLSocket", :write => nil, :flush => nil, :close => nil)
157
+ @tcp_socket = mock("TCPSocket", :close => nil)
158
+ @connection.stub(:setup_ssl_context)
159
+ @connection.stub(:connect_socket).and_return([@tcp_socket, @ssl_socket])
160
+ @connection.stub(:check_for_error)
161
+ @connection.connect
162
+ end
163
+
164
+ it "should write the data to the SSL socket" do
165
+ @ssl_socket.should_receive(:write).with("blah")
166
+ @connection.write("blah")
167
+ end
168
+
169
+ it "should flush the SSL socket" do
170
+ @ssl_socket.should_receive(:flush)
171
+ @connection.write("blah")
172
+ end
173
+
174
+ it "should select check for an error packet" do
175
+ @connection.should_receive(:check_for_error)
176
+ @connection.write("blah")
177
+ end
178
+ end
179
+
180
+ describe Rapns::Daemon::Connection, "when receiving an error packet" do
181
+ before do
182
+ @notification = Rapns::Notification.create!(:device_token => "a" * 64)
183
+ @notification.stub(:save!)
184
+ @connection = Rapns::Daemon::Connection.new("Connection 1")
185
+ @ssl_socket = mock("SSLSocket", :write => nil, :flush => nil, :close => nil, :read => [8, 4, @notification.id].pack("ccN"))
186
+ @connection.stub(:setup_ssl_context)
187
+ @connection.stub(:connect_socket).and_return([@tcp_socket, @ssl_socket])
188
+ IO.stub(:select).and_return([@ssl_socket, [], []])
189
+ logger = mock("Logger", :error => nil, :warn => nil)
190
+ Rapns::Daemon.stub(:logger).and_return(logger)
191
+ @connection.connect
192
+ end
193
+
194
+ it "should raise a DeliveryError when an error is received" do
195
+ expect { @connection.write("msg with an error") }.should raise_error(Rapns::DeliveryError)
196
+ end
197
+
198
+ it "should not raise a DeliveryError if the packet cmd value is not 8" do
199
+ @ssl_socket.stub(:read).and_return([6, 4, 12].pack("ccN"))
200
+ expect { @connection.write("msg with an error") }.should_not raise_error(Rapns::DeliveryError)
201
+ end
202
+
203
+ it "should not raise a DeliveryError if the status code is 0 (no error)" do
204
+ @ssl_socket.stub(:read).and_return([8, 0, 12].pack("ccN"))
205
+ expect { @connection.write("msg with an error") }.should_not raise_error(Rapns::DeliveryError)
206
+ end
207
+
208
+ it "should read 6 bytes from the socket" do
209
+ @ssl_socket.should_receive(:read).with(6).and_return(nil)
210
+ @connection.write("msg with an error")
211
+ end
212
+
213
+ it "should not attempt to read from the socket if the socket was not selected for reading after the timeout" do
214
+ IO.stub(:select).and_return(nil)
215
+ @ssl_socket.should_not_receive(:read)
216
+ @connection.write("msg with an error")
217
+ end
218
+
219
+ it "should not raise a DeliveryError if the socket read returns nothing" do
220
+ @ssl_socket.stub(:read).with(6).and_return(nil)
221
+ expect { @connection.write("msg with an error") }.should_not raise_error(Rapns::DeliveryError)
222
+ end
223
+
224
+ it "should close the socket after handling the error" do
225
+ @connection.should_receive(:close)
226
+ begin
227
+ @connection.write("msg with an error")
228
+ rescue Rapns::DeliveryError
229
+ end
230
+ end
231
+
232
+ it "should reconnect the socket" do
233
+ @connection.should_receive(:connect_socket)
234
+ begin
235
+ @connection.write("msg with an error")
236
+ rescue Rapns::DeliveryError
237
+ end
238
+ end
239
+
240
+ it "should log that the connection is being reconnected" do
241
+ Rapns::Daemon.logger.should_receive(:warn).with("[Connection 1] Error received, reconnecting...")
242
+ begin
243
+ @connection.write("msg with an error")
244
+ rescue Rapns::DeliveryError
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,11 @@
1
+ require "spec_helper"
2
+
3
+ describe Rapns::DeliveryError do
4
+ before do
5
+ @error = Rapns::DeliveryError.new(4, "Missing payload", 12)
6
+ end
7
+
8
+ it "should give an informative message" do
9
+ @error.message.should == "Unable to deliver notification 12, received APN error 4 (Missing payload)"
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ require "spec_helper"
2
+
3
+ describe Rapns::Daemon::DeliveryHandlerPool do
4
+ before do
5
+ @handler = mock("DeliveryHandler", :start => nil)
6
+ Rapns::Daemon::DeliveryHandler.stub(:new).and_return(@handler)
7
+ @pool = Rapns::Daemon::DeliveryHandlerPool.new(3)
8
+ Rapns::Daemon.stub(:delivery_queue).and_return(mock("Delivery queue", :push => nil))
9
+ end
10
+
11
+ it "should populate the pool" do
12
+ Rapns::Daemon::DeliveryHandler.should_receive(:new).exactly(3).times
13
+ @pool.populate
14
+ end
15
+
16
+ it "should tell each connection to close when drained" do
17
+ @pool.populate
18
+ @handler.should_receive(:stop).exactly(3).times
19
+ @pool.drain
20
+ end
21
+
22
+ it "should initiate the topping process for each DeliveryHandler before the pool is drained" do
23
+ Rapns::Daemon.delivery_queue.should_receive(:push).with(0x666).exactly(3).times
24
+ @pool.drain
25
+ end
26
+ end