rapns 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +105 -0
- data/bin/rapns +26 -0
- data/lib/generators/rapns_generator.rb +16 -0
- data/lib/generators/templates/create_rapns_notifications.rb +26 -0
- data/lib/generators/templates/rapns.yml +17 -0
- data/lib/rapns/binary_notification_validator.rb +10 -0
- data/lib/rapns/daemon/certificate.rb +27 -0
- data/lib/rapns/daemon/configuration.rb +69 -0
- data/lib/rapns/daemon/connection.rb +99 -0
- data/lib/rapns/daemon/connection_pool.rb +31 -0
- data/lib/rapns/daemon/delivery_error.rb +15 -0
- data/lib/rapns/daemon/delivery_handler.rb +53 -0
- data/lib/rapns/daemon/delivery_handler_pool.rb +24 -0
- data/lib/rapns/daemon/feeder.rb +31 -0
- data/lib/rapns/daemon/logger.rb +49 -0
- data/lib/rapns/daemon/pool.rb +41 -0
- data/lib/rapns/daemon.rb +76 -0
- data/lib/rapns/notification.rb +44 -0
- data/lib/rapns/version.rb +3 -0
- data/lib/rapns.rb +5 -0
- data/spec/rapns/daemon/certificate_spec.rb +16 -0
- data/spec/rapns/daemon/configuration_spec.rb +125 -0
- data/spec/rapns/daemon/connection_pool_spec.rb +40 -0
- data/spec/rapns/daemon/connection_spec.rb +247 -0
- data/spec/rapns/daemon/delivery_error_spec.rb +11 -0
- data/spec/rapns/daemon/delivery_handler_pool_spec.rb +26 -0
- data/spec/rapns/daemon/delivery_handler_spec.rb +110 -0
- data/spec/rapns/daemon/feeder_spec.rb +61 -0
- data/spec/rapns/daemon/logger_spec.rb +96 -0
- data/spec/rapns/daemon_spec.rb +141 -0
- data/spec/rapns/notification_spec.rb +112 -0
- data/spec/spec_helper.rb +25 -0
- metadata +91 -0
data/lib/rapns/daemon.rb
ADDED
@@ -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
|
data/lib/rapns.rb
ADDED
@@ -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
|