rapns 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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