Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Feedback callback implementation #78

Merged
merged 3 commits into from

2 participants

@mattconnolly

Implementation for #77: Feedback received callback.

@ileitch
Owner

Looks good. Can you rebase so Travis can build?

mattconnolly and others added some commits
@mattconnolly mattconnolly Adding validation & spec for ensuring that Rapns::App instances conta…
…in valid certificate data.
70a4b6c
Matt Connolly Fix typoes in specs db18892
Matt Connolly Adding Rapns::Config class; Adding support for Config to be modified …
…from the rails app 'config/initializers/rapns.rb' file; Adding Feedback receiver call a callback when feedback is received.

For issue #77
3fd15dd
@mattconnolly

Rebased.

@mattconnolly

I suppose next step would be to update the rails generator to create a template config/initializers/rapns.rb file with examples/comments...

@ileitch ileitch merged commit c278ef9 into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 15, 2012
  1. @mattconnolly
  2. @mattconnolly

    Fix typoes in specs

    Matt Connolly authored mattconnolly committed
  3. @mattconnolly

    Adding Rapns::Config class; Adding support for Config to be modified …

    Matt Connolly authored mattconnolly committed
    …from the rails app 'config/initializers/rapns.rb' file; Adding Feedback receiver call a callback when feedback is received.
    
    For issue #77
This page is out of date. Refresh to see the latest.
View
9 bin/rapns
@@ -5,13 +5,7 @@ require 'rapns'
environment = ARGV[0]
-config = Struct.new(:foreground, :push_poll, :feedback_poll, :airbrake_notify, :check_for_errors, :pid_file, :batch_size).new
-config.foreground = false
-config.push_poll = 2
-config.feedback_poll = 60
-config.airbrake_notify = true
-config.check_for_errors = true
-config.batch_size = 5000
+config = Rapns.configuration
banner = 'Usage: rapns <Rails environment> [options]'
ARGV.options do |opts|
@@ -35,6 +29,7 @@ end
ENV['RAILS_ENV'] = environment
load 'config/environment.rb'
+load 'config/initializers/rapns.rb' if File.exist?('config/initializers/rapns.rb')
require 'rapns/daemon'
require 'rapns/patches'
View
1  lib/rapns.rb
@@ -7,3 +7,4 @@
require 'rapns/notification'
require 'rapns/feedback'
require 'rapns/app'
+require 'rapns/config'
View
20 lib/rapns/app.rb
@@ -8,5 +8,23 @@ class App < ActiveRecord::Base
validates :environment, :presence => true, :inclusion => { :in => %w(development production) }
validates :certificate, :presence => true
validates_numericality_of :connections, :greater_than => 0, :only_integer => true
+
+ validate :certificate_has_matching_private_key
+
+ private
+
+ def certificate_has_matching_private_key
+ result = false
+ if certificate.present?
+ x509 = OpenSSL::X509::Certificate.new certificate rescue nil
+ pkey = OpenSSL::PKey::RSA.new certificate rescue nil
+ result = !x509.nil? && !pkey.nil?
+ unless result
+ errors.add :certificate, "Certificate value must contain a certificate and a private key"
+ end
+ end
+ result
+ end
end
-end
+end
+
View
55 lib/rapns/config.rb
@@ -0,0 +1,55 @@
+module Rapns
+
+ # A globally accessible instance of Rapns::Config
+ def self.configuration
+ @configuration ||= Rapns::Config.new
+ end
+
+ # Call the given block yielding to it the global Rapns::Config instance for setting
+ # configuration values / callbacks.
+ #
+ # Typically this would be used in your Rails application's config/initializers/rapns.rb file
+ def self.configure
+ yield configuration if block_given?
+ end
+
+ # A class to hold Rapns configuration settings and callbacks.
+ class Config < Struct.new(:foreground, :push_poll, :feedback_poll, :airbrake_notify, :check_for_errors, :pid_file, :batch_size)
+
+ attr_accessor :feedback_callback
+
+ # Initialize the Config with default values
+ def initialize
+ super
+
+ # defaults:
+ self.foreground = false
+ self.push_poll = 2
+ self.feedback_poll = 60
+ self.airbrake_notify = true
+ self.check_for_errors = true
+ self.batch_size = 5000
+ end
+
+ # Define a block that will be executed with a Rapns::Feedback instance when feedback has been received from the
+ # push notification servers that a notification has failed to be delivered. Further notifications should not
+ # be sent to this device token.
+ #
+ # Example usage (in config/initializers/rapns.rb):
+ #
+ # Rapns.configure do |config|
+ # config.on_feedback do |feedback|
+ # device = Device.find_by_device_token feedback.device_token
+ # if device
+ # device.active = false
+ # device.save
+ # end
+ # end
+ # end
+ #
+ # Where `Device` is a model specific to your Rails app that has a `device_token` field.
+ def on_feedback(&block)
+ self.feedback_callback = block
+ end
+ end
+end
View
7 lib/rapns/daemon/feedback_receiver.rb
@@ -69,7 +69,12 @@ def create_feedback(failed_at, device_token)
formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
with_database_reconnect_and_retry do
Rapns::Daemon.logger.info("[FeedbackReceiver:#{@name}] Delivery failed at #{formatted_failed_at} for #{device_token}")
- Rapns::Feedback.create!(:failed_at => failed_at, :device_token => device_token, :app => @name)
+ feedback = Rapns::Feedback.create!(:failed_at => failed_at, :device_token => device_token, :app => @name)
+ begin
+ Rapns.configuration.feedback_callback.call(feedback) if Rapns.configuration.feedback_callback
+ rescue Exception => e
+ Rapns::Daemon.logger.error(e)
+ end
end
end
end
View
89 spec/rapns/app_spec.rb
@@ -0,0 +1,89 @@
+require "spec_helper"
+
+
+# a test certificate that contains both an X509 certificate and
+# a private key, similar to those used for connecting to Apple
+# push notification servers.
+#
+# Note that we cannot validate the certificate and private key
+# because we are missing the certificate chain used to validate
+# the certificate, and this is private to Apple. So if the app
+# has a certificate and a private key in it, the only way to find
+# out if it really is valid is to connect to Apple's servers.
+#
+TEST_CERT = <<EOF
+Bag Attributes
+ friendlyName: test certificate
+ localKeyID: 00 93 8F E4 A3 C3 75 64 3D 7E EA 14 0B 0A EA DD 15 85 8A D5
+subject=/CN=test certificate/O=Example/OU=Example/ST=QLD/C=AU/L=Example/emailAddress=user@example.com
+issuer=/CN=test certificate/O=Example/OU=Example/ST=QLD/C=AU/L=Example/emailAddress=user@example.com
+-----BEGIN CERTIFICATE-----
+MIID5jCCAs6gAwIBAgIBATALBgkqhkiG9w0BAQswgY0xGTAXBgNVBAMMEHRlc3Qg
+Y2VydGlmaWNhdGUxEDAOBgNVBAoMB0V4YW1wbGUxEDAOBgNVBAsMB0V4YW1wbGUx
+DDAKBgNVBAgMA1FMRDELMAkGA1UEBhMCQVUxEDAOBgNVBAcMB0V4YW1wbGUxHzAd
+BgkqhkiG9w0BCQEWEHVzZXJAZXhhbXBsZS5jb20wHhcNMTIwOTA5MDMxODMyWhcN
+MjIwOTA3MDMxODMyWjCBjTEZMBcGA1UEAwwQdGVzdCBjZXJ0aWZpY2F0ZTEQMA4G
+A1UECgwHRXhhbXBsZTEQMA4GA1UECwwHRXhhbXBsZTEMMAoGA1UECAwDUUxEMQsw
+CQYDVQQGEwJBVTEQMA4GA1UEBwwHRXhhbXBsZTEfMB0GCSqGSIb3DQEJARYQdXNl
+ckBleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKF+
+UDsN1sLen8g+97PNTiWju9+wkSv+H5rQlvb6YFLPx11YvqpK8ms6kFU1OmWeLfmh
+cpsT+bZtKupC7aGPoSG3RXzzf/YUMgs/ZSXA0idZHA6tkReAEzIX6jL5otfPWbaP
+luCTUoVMeP4u9ywk628zlqh9IQHC1Agl0R1xGCpULDk8kn1gPyEisl38wI5aDbzy
+6lYQGNUKOqt1xfVjtIFe/jyY/v0sxFjIJlRLcAFBuJx4sRV+PwRBkusOQtYwcwpI
+loMxJj+GQe66ueATW81aC4iOU66DAFFEuGzwIwm3bOilimGGQbGb92F339RfmSOo
+TPAvVhsakI3mzESb4lkCAwEAAaNRME8wDgYDVR0PAQH/BAQDAgeAMCAGA1UdJQEB
+/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAbBgNVHREEFDASgRB1c2VyQGV4YW1w
+bGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQA5UbNR+83ZdI2DiaB4dRmy0V5RDAqJ
+k9+QskcTV4gBTjsOBS46Dw1tI6iTrfTyjYJdnyH0Y2Y2YVWBnvtON41UCZak+4ed
+/IqyzU0dtfZ+frWa0RY4reyl80TwqnzyJfni0nDo4zGGvz70cxyaz2u1BWqwLjqb
+dh8Dxvt+aHW2MQi0iGKh/HNbgwVanR4+ubNwziK9sR1Rnq9MkHWtwBw16SXQG6ao
+SZKASWNaH8VL08Zz0E98cwd137UJkPsldCwJ8kHR5OzkcjPdXvnGD3d64yy2TC1Z
+Gy1Aazt98wPcTYBytlhK8Rvzg9OoY9QmsdpmWxz1ZCXECJNqCa3IKsqO
+-----END CERTIFICATE-----
+Bag Attributes
+ friendlyName: test certificate
+ localKeyID: 00 93 8F E4 A3 C3 75 64 3D 7E EA 14 0B 0A EA DD 15 85 8A D5
+Key Attributes: <No Attributes>
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAoX5QOw3Wwt6fyD73s81OJaO737CRK/4fmtCW9vpgUs/HXVi+
+qkryazqQVTU6ZZ4t+aFymxP5tm0q6kLtoY+hIbdFfPN/9hQyCz9lJcDSJ1kcDq2R
+F4ATMhfqMvmi189Zto+W4JNShUx4/i73LCTrbzOWqH0hAcLUCCXRHXEYKlQsOTyS
+fWA/ISKyXfzAjloNvPLqVhAY1Qo6q3XF9WO0gV7+PJj+/SzEWMgmVEtwAUG4nHix
+FX4/BEGS6w5C1jBzCkiWgzEmP4ZB7rq54BNbzVoLiI5TroMAUUS4bPAjCbds6KWK
+YYZBsZv3YXff1F+ZI6hM8C9WGxqQjebMRJviWQIDAQABAoIBAQCTiLIDQUFSBdAz
+QFNLD+S0vkCEuunlJuP4q1c/ir006l1YChsluBJ/o6D4NwiCjV+zDquEwVsALftm
+yH4PewfZpXT2Ef508T5GyEO/mchj6iSXxDkpHvhqay6qIyWBwwxSnBtaTzy0Soi+
+rmlhCtmLXbXld2sQEM1kJChGnWtWPtvSyrn+mapNPZviGRtgRNK+YsrAti1nUext
+2syO5mTdHf1D8GR7I98OaX6odREuSocEV9PzfapWZx2GK5tvRiS1skiug5ciieTd
+Am5/C+bb31h4drFslihLb5BRGO5SFQJvMJL2Sx1f19BCC4XikS01P4/zZbxQNq79
+kxEQuDGBAoGBANP4pIYZ5xshCkx7cTYqmxzWLClGKE2S7Oa8N89mtOwfmqT9AFun
+t9Us9Ukbi8BaKlKhGpQ1HlLf/KVcpyW0x2qLou6AyIWYH+/5VaR3graNgUnzpK9f
+1F5HoaNHbhlAoebqhzhASFlJI2aqUdQjdOv73z+s9szJU4gpILNwGDFnAoGBAMMJ
+j+vIxtG9J2jldyoXzpg5mbMXSj9u/wFLBVdjXWyOoiqVMMBto53RnoqAom7Ifr9D
+49LxRAT1Q3l4vs/YnM3ziMsIg2vQK1EbrLsY9OnD/kvPaLXOlNIOdfLM8UeVWZMc
+I4LPbbZrhv/7CC8RjbRhMoWWdGYPvxmvD6V4ZDY/AoGBALoI6OxA45Htx4okdNHj
+RstiNNPsnQaoQn6nBhxiubraafEPkzbd1fukP4pwQJELEUX/2sHkdL6rkqLW1GPF
+a5dZAiBsqpCFWNJWdBGqSfBJ9QSgbxLz+gDcwUH6OOi0zuNJRm/aCyVBiW5bYQHc
+NIvAPMk31ksZDtTbs7WIVdNVAoGBALZ1+KWNxKqs+fSBT5UahpUUtfy8miJz9a7A
+/3M8q0cGvSF3Rw+OwpW/aEGMi+l2OlU27ykFuyukRAac9m296RwnbF79TO2M5ylO
+6a5zb5ROXlWP6RbE96b4DlIidssQJqegmHwlEC+rsrVBpOtb0aThlYEyOxzMOGyP
+wOR9l8rDAoGADZ4TUHFM6VrvPlUZBkGbqiyXH9IM/y9JWk+22JQCEGnM6RFZemSs
+jxWqQiPAdJtb3xKryJSCMtFPH9azedoCrSgaMflJ1QgoXgpiKZyoEXWraVUggh/0
+CEavgZcTZ6SvMuayqJdGGB+zb1V8XwXMtCjApR/kTm47DjxO4DmpOPs=
+-----END RSA PRIVATE KEY-----
+EOF
+
+
+describe Rapns::App do
+
+ it "does not validate an app with an invalid certificate" do
+ @app = Rapns::App.new :key => 'test', :environment => 'development', :certificate => 'foo'
+ @app.should_not be_valid
+ @app.errors.should include(:certificate)
+ end
+
+ it "does validate a real certificate" do
+ @app = Rapns::App.new :key => 'test', :environment => 'development', :certificate => TEST_CERT
+ @app.should be_valid
+ end
+end
View
66 spec/rapns/daemon/feedback_receiver_spec.rb
@@ -9,14 +9,14 @@
let(:app) { 'my_app' }
let(:connection) { stub(:connect => nil, :read => nil, :close => nil) }
let(:logger) { stub(:error => nil, :info => nil) }
- let(:receiever) { Rapns::Daemon::FeedbackReceiver.new(app, host, port, poll, certificate, password) }
+ let(:receiver) { Rapns::Daemon::FeedbackReceiver.new(app, host, port, poll, certificate, password) }
before do
- receiever.stub(:interruptible_sleep)
+ receiver.stub(:interruptible_sleep)
Rapns::Daemon.logger = logger
Rapns::Daemon::Connection.stub(:new => connection)
Rapns::Feedback.stub(:create!)
- receiever.instance_variable_set("@stop", false)
+ receiver.instance_variable_set("@stop", false)
end
def stub_connection_read_with_tuple
@@ -32,61 +32,87 @@ def connection.read(bytes)
it 'instantiates a new connection' do
Rapns::Daemon::Connection.should_receive(:new).with("FeedbackReceiver:#{app}", host, port, certificate, password)
- receiever.check_for_feedback
+ receiver.check_for_feedback
end
it 'connects to the feeback service' do
connection.should_receive(:connect)
- receiever.check_for_feedback
+ receiver.check_for_feedback
end
it 'closes the connection' do
connection.should_receive(:close)
- receiever.check_for_feedback
+ receiver.check_for_feedback
end
it 'reads from the connection' do
connection.should_receive(:read).with(38)
- receiever.check_for_feedback
+ receiver.check_for_feedback
end
it 'logs the feedback' do
stub_connection_read_with_tuple
Rapns::Daemon.logger.should_receive(:info).with("[FeedbackReceiver:my_app] Delivery failed at 2011-12-10 16:08:45 UTC for 834f786655eb9f84614a05ad7d00af31e5cfe93ac3ea078f1da44d2a4eb0ce17")
- receiever.check_for_feedback
+ receiver.check_for_feedback
end
it 'creates the feedback' do
stub_connection_read_with_tuple
Rapns::Feedback.should_receive(:create!).with(:failed_at => Time.at(1323533325), :device_token => '834f786655eb9f84614a05ad7d00af31e5cfe93ac3ea078f1da44d2a4eb0ce17', :app => 'my_app')
- receiever.check_for_feedback
+ receiver.check_for_feedback
end
it 'logs errors' do
error = StandardError.new('bork!')
connection.stub(:read).and_raise(error)
Rapns::Daemon.logger.should_receive(:error).with(error)
- lambda { receiever.check_for_feedback }.should raise_error
+ lambda { receiver.check_for_feedback }.should raise_error
end
it 'sleeps for the feedback poll period' do
- receiever.stub(:check_for_feedback)
- receiever.should_receive(:interruptible_sleep).with(60).at_least(:once)
+ receiver.stub(:check_for_feedback)
+ receiver.should_receive(:interruptible_sleep).with(60).at_least(:once)
Thread.stub(:new).and_yield
- receiever.stub(:loop).and_yield
- receiever.start
+ receiver.stub(:loop).and_yield
+ receiver.start
end
it 'checks for feedback when started' do
- receiever.should_receive(:check_for_feedback).at_least(:once)
+ receiver.should_receive(:check_for_feedback).at_least(:once)
Thread.stub(:new).and_yield
- receiever.stub(:loop).and_yield
- receiever.start
+ receiver.stub(:loop).and_yield
+ receiver.start
end
it 'interrupts sleep when stopped' do
- receiever.stub(:check_for_feedback)
- receiever.should_receive(:interrupt_sleep)
- receiever.stop
+ receiver.stub(:check_for_feedback)
+ receiver.should_receive(:interrupt_sleep)
+ receiver.stop
+ end
+
+ it 'calls the configuration feedback_callback when feedback is received and the callback is set' do
+ stub_connection_read_with_tuple
+ Rapns::configuration.feedback_callback = Proc.new {}
+ feedback = Object.new
+ Rapns::Feedback.stub(:create! => feedback)
+ Rapns::configuration.feedback_callback.should_receive(:call).with(feedback)
+ receiver.check_for_feedback
+ end
+
+ it 'catches exceptions in the feedback_callback' do
+ error = StandardError.new('bork!')
+ stub_connection_read_with_tuple
+ callback = Proc.new { raise error }
+ Rapns::configuration.feedback_callback = callback
+ expect { receiver.check_for_feedback }.not_to raise_error
+ end
+
+ it 'logs an exception from the feedback_callback' do
+ error = StandardError.new('bork!')
+ stub_connection_read_with_tuple
+ callback = Proc.new { raise error }
+ Rapns::Daemon.logger.should_receive(:error).with(error)
+ Rapns::configuration.feedback_callback = callback
+ receiver.check_for_feedback
end
end
View
6 spec/rapns/feedback_spec.rb
@@ -5,8 +5,8 @@
it { should validate_presence_of(:failed_at) }
it "should validate the format of the device_token" do
- notification = Rapns::Feedback.new(:device_token => "{$%^&*()}")
- notification.valid?.should be_false
- notification.errors[:device_token].include?("is invalid").should be_true
+ feedback = Rapns::Feedback.new(:device_token => "{$%^&*()}")
+ feedback.valid?.should be_false
+ feedback.errors[:device_token].include?("is invalid").should be_true
end
end
Something went wrong with that request. Please try again.