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.
- 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
|