rapns 3.0.0-java

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.
Files changed (89) hide show
  1. data/CHANGELOG.md +31 -0
  2. data/LICENSE +7 -0
  3. data/README.md +138 -0
  4. data/bin/rapns +41 -0
  5. data/config/database.yml +39 -0
  6. data/lib/generators/rapns_generator.rb +34 -0
  7. data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
  8. data/lib/generators/templates/add_app_to_rapns.rb +11 -0
  9. data/lib/generators/templates/add_gcm.rb +94 -0
  10. data/lib/generators/templates/create_rapns_apps.rb +16 -0
  11. data/lib/generators/templates/create_rapns_feedback.rb +15 -0
  12. data/lib/generators/templates/create_rapns_notifications.rb +26 -0
  13. data/lib/generators/templates/rapns.rb +39 -0
  14. data/lib/rapns/apns/app.rb +8 -0
  15. data/lib/rapns/apns/binary_notification_validator.rb +12 -0
  16. data/lib/rapns/apns/device_token_format_validator.rb +12 -0
  17. data/lib/rapns/apns/feedback.rb +14 -0
  18. data/lib/rapns/apns/notification.rb +86 -0
  19. data/lib/rapns/apns/required_fields_validator.rb +14 -0
  20. data/lib/rapns/app.rb +29 -0
  21. data/lib/rapns/configuration.rb +46 -0
  22. data/lib/rapns/daemon/apns/app_runner.rb +36 -0
  23. data/lib/rapns/daemon/apns/connection.rb +113 -0
  24. data/lib/rapns/daemon/apns/delivery.rb +63 -0
  25. data/lib/rapns/daemon/apns/delivery_handler.rb +21 -0
  26. data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
  27. data/lib/rapns/daemon/apns/feedback_receiver.rb +74 -0
  28. data/lib/rapns/daemon/app_runner.rb +135 -0
  29. data/lib/rapns/daemon/database_reconnectable.rb +57 -0
  30. data/lib/rapns/daemon/delivery.rb +43 -0
  31. data/lib/rapns/daemon/delivery_error.rb +19 -0
  32. data/lib/rapns/daemon/delivery_handler.rb +46 -0
  33. data/lib/rapns/daemon/delivery_queue.rb +42 -0
  34. data/lib/rapns/daemon/delivery_queue_18.rb +44 -0
  35. data/lib/rapns/daemon/delivery_queue_19.rb +42 -0
  36. data/lib/rapns/daemon/feeder.rb +37 -0
  37. data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
  38. data/lib/rapns/daemon/gcm/delivery.rb +206 -0
  39. data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
  40. data/lib/rapns/daemon/interruptible_sleep.rb +18 -0
  41. data/lib/rapns/daemon/logger.rb +68 -0
  42. data/lib/rapns/daemon.rb +136 -0
  43. data/lib/rapns/gcm/app.rb +7 -0
  44. data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
  45. data/lib/rapns/gcm/notification.rb +31 -0
  46. data/lib/rapns/gcm/payload_size_validator.rb +13 -0
  47. data/lib/rapns/multi_json_helper.rb +16 -0
  48. data/lib/rapns/notification.rb +54 -0
  49. data/lib/rapns/patches/rails/3.1.0/postgresql_adapter.rb +12 -0
  50. data/lib/rapns/patches/rails/3.1.1/postgresql_adapter.rb +17 -0
  51. data/lib/rapns/patches.rb +6 -0
  52. data/lib/rapns/version.rb +3 -0
  53. data/lib/rapns.rb +21 -0
  54. data/lib/tasks/cane.rake +18 -0
  55. data/lib/tasks/test.rake +33 -0
  56. data/spec/acceptance/gcm_upgrade_spec.rb +34 -0
  57. data/spec/acceptance_spec_helper.rb +85 -0
  58. data/spec/support/simplecov_helper.rb +13 -0
  59. data/spec/support/simplecov_quality_formatter.rb +8 -0
  60. data/spec/unit/apns/app_spec.rb +15 -0
  61. data/spec/unit/apns/feedback_spec.rb +12 -0
  62. data/spec/unit/apns/notification_spec.rb +198 -0
  63. data/spec/unit/app_spec.rb +18 -0
  64. data/spec/unit/configuration_spec.rb +38 -0
  65. data/spec/unit/daemon/apns/app_runner_spec.rb +39 -0
  66. data/spec/unit/daemon/apns/connection_spec.rb +234 -0
  67. data/spec/unit/daemon/apns/delivery_handler_spec.rb +48 -0
  68. data/spec/unit/daemon/apns/delivery_spec.rb +160 -0
  69. data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
  70. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +118 -0
  71. data/spec/unit/daemon/app_runner_shared.rb +66 -0
  72. data/spec/unit/daemon/app_runner_spec.rb +129 -0
  73. data/spec/unit/daemon/database_reconnectable_spec.rb +109 -0
  74. data/spec/unit/daemon/delivery_error_spec.rb +13 -0
  75. data/spec/unit/daemon/delivery_handler_shared.rb +28 -0
  76. data/spec/unit/daemon/delivery_queue_spec.rb +29 -0
  77. data/spec/unit/daemon/feeder_spec.rb +95 -0
  78. data/spec/unit/daemon/gcm/app_runner_spec.rb +17 -0
  79. data/spec/unit/daemon/gcm/delivery_handler_spec.rb +36 -0
  80. data/spec/unit/daemon/gcm/delivery_spec.rb +236 -0
  81. data/spec/unit/daemon/interruptible_sleep_spec.rb +40 -0
  82. data/spec/unit/daemon/logger_spec.rb +156 -0
  83. data/spec/unit/daemon_spec.rb +139 -0
  84. data/spec/unit/gcm/app_spec.rb +5 -0
  85. data/spec/unit/gcm/notification_spec.rb +55 -0
  86. data/spec/unit/notification_shared.rb +38 -0
  87. data/spec/unit/notification_spec.rb +6 -0
  88. data/spec/unit_spec_helper.rb +145 -0
  89. metadata +240 -0
@@ -0,0 +1,129 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Rapns::Daemon::AppRunner, 'stop' do
4
+ let(:runner) { stub }
5
+ before { Rapns::Daemon::AppRunner.runners['app'] = runner }
6
+ after { Rapns::Daemon::AppRunner.runners.clear }
7
+
8
+ it 'stops all runners' do
9
+ runner.should_receive(:stop)
10
+ Rapns::Daemon::AppRunner.stop
11
+ end
12
+ end
13
+
14
+ describe Rapns::Daemon::AppRunner, 'deliver' do
15
+ let(:runner) { stub }
16
+ let(:notification) { stub(:app_id => 1) }
17
+ let(:logger) { stub(:error => nil) }
18
+
19
+ before do
20
+ Rapns::Daemon.stub(:logger => logger)
21
+ Rapns::Daemon::AppRunner.runners[1] = runner
22
+ end
23
+
24
+ after { Rapns::Daemon::AppRunner.runners.clear }
25
+
26
+ it 'enqueues the notification' do
27
+ runner.should_receive(:enqueue).with(notification)
28
+ Rapns::Daemon::AppRunner.enqueue(notification)
29
+ end
30
+
31
+ it 'logs an error if there is no runner to deliver the notification' do
32
+ notification.stub(:app_id => 2, :id => 123)
33
+ logger.should_receive(:error).with("No such app '#{notification.app_id}' for notification #{notification.id}.")
34
+ Rapns::Daemon::AppRunner.enqueue(notification)
35
+ end
36
+ end
37
+
38
+ describe Rapns::Daemon::AppRunner, 'sync' do
39
+ let(:app) { Rapns::Apns::App.new }
40
+ let(:new_app) { Rapns::Apns::App.new }
41
+ let(:runner) { stub(:sync => nil, :stop => nil, :start => nil) }
42
+ let(:logger) { stub(:error => nil) }
43
+ let(:queue) { Rapns::Daemon::DeliveryQueue.new }
44
+
45
+ before do
46
+ app.stub(:id => 1)
47
+ new_app.stub(:id => 2)
48
+ Rapns::Daemon::DeliveryQueue.stub(:new => queue)
49
+ Rapns::Daemon::AppRunner.runners[app.id] = runner
50
+ Rapns::App.stub(:all => [app])
51
+ end
52
+
53
+ after { Rapns::Daemon::AppRunner.runners.clear }
54
+
55
+ it 'loads all apps' do
56
+ Rapns::App.should_receive(:all)
57
+ Rapns::Daemon::AppRunner.sync
58
+ end
59
+
60
+ it 'instructs existing runners to sync' do
61
+ runner.should_receive(:sync).with(app)
62
+ Rapns::Daemon::AppRunner.sync
63
+ end
64
+
65
+ it 'starts a runner for a new app' do
66
+ Rapns::App.stub(:all => [app, new_app])
67
+ new_runner = stub
68
+ Rapns::Daemon::Apns::AppRunner.should_receive(:new).with(new_app).and_return(new_runner)
69
+ new_runner.should_receive(:start)
70
+ Rapns::Daemon::AppRunner.sync
71
+ end
72
+
73
+ it 'deletes old apps' do
74
+ Rapns::App.stub(:all => [])
75
+ runner.should_receive(:stop)
76
+ Rapns::Daemon::AppRunner.sync
77
+ end
78
+
79
+ it 'logs an error if the app could not be started' do
80
+ Rapns::App.stub(:all => [app, new_app])
81
+ new_runner = stub
82
+ Rapns::Daemon::Apns::AppRunner.should_receive(:new).with(new_app).and_return(new_runner)
83
+ new_runner.stub(:start).and_raise(StandardError)
84
+ Rapns::Daemon.logger.should_receive(:error).any_number_of_times
85
+ Rapns::Daemon::AppRunner.sync
86
+ end
87
+ end
88
+
89
+ describe Rapns::Daemon::AppRunner, 'debug' do
90
+ let!(:app) { Rapns::Apns::App.create!(:name => 'test', :connections => 1,
91
+ :environment => 'development', :certificate => TEST_CERT) }
92
+ let(:logger) { stub(:info => nil) }
93
+
94
+ before do
95
+ Rapns::Daemon.stub(:config => {})
96
+ Rapns::Daemon::Apns::FeedbackReceiver.stub(:new => stub.as_null_object)
97
+ Rapns::Daemon::Apns::Connection.stub(:new => stub.as_null_object)
98
+ Rapns::Daemon.stub(:logger => logger)
99
+ Rapns::Daemon::AppRunner.sync
100
+ end
101
+
102
+ after { Rapns::Daemon::AppRunner.runners.clear }
103
+
104
+ it 'prints debug app states to the log' do
105
+ Rapns::Daemon.logger.should_receive(:info).with("\ntest:\n handlers: 1\n queued: 0\n idle: true\n")
106
+ Rapns::Daemon::AppRunner.debug
107
+ end
108
+ end
109
+
110
+ describe Rapns::Daemon::AppRunner, 'idle' do
111
+ let!(:app) { Rapns::Apns::App.create!(:name => 'test', :connections => 1,
112
+ :environment => 'development', :certificate => TEST_CERT) }
113
+ let(:logger) { stub(:info => nil) }
114
+
115
+ before do
116
+ Rapns::Daemon.stub(:config => {})
117
+ Rapns::Daemon::Apns::FeedbackReceiver.stub(:new => stub.as_null_object)
118
+ Rapns::Daemon::Apns::Connection.stub(:new => stub.as_null_object)
119
+ Rapns::Daemon.stub(:logger => logger)
120
+ Rapns::Daemon::AppRunner.sync
121
+ end
122
+
123
+ after { Rapns::Daemon::AppRunner.runners.clear }
124
+
125
+ it 'returns idle runners' do
126
+ runner = Rapns::Daemon::AppRunner.runners[app.id]
127
+ Rapns::Daemon::AppRunner.idle.should == [runner]
128
+ end
129
+ end
@@ -0,0 +1,109 @@
1
+ require "unit_spec_helper"
2
+
3
+ describe Rapns::Daemon::DatabaseReconnectable do
4
+ class TestDouble
5
+ include Rapns::Daemon::DatabaseReconnectable
6
+
7
+ attr_reader :name
8
+
9
+ def initialize(error, max_calls)
10
+ @error = error
11
+ @max_calls = max_calls
12
+ @calls = 0
13
+ end
14
+
15
+ def perform
16
+ with_database_reconnect_and_retry do
17
+ @calls += 1
18
+ raise @error if @calls <= @max_calls
19
+ end
20
+ end
21
+ end
22
+
23
+ let(:adapter_error_class) do
24
+ case $adapter
25
+ when 'postgresql'
26
+ PGError
27
+ when 'mysql'
28
+ Mysql::Error
29
+ when 'mysql2'
30
+ Mysql2::Error
31
+ when 'jdbcpostgresql'
32
+ ActiveRecord::JDBCError
33
+ when 'jdbcmysql'
34
+ ActiveRecord::JDBCError
35
+ else
36
+ raise "Please update #{__FILE__} for adapter #{$adapter}"
37
+ end
38
+ end
39
+ let(:error) { adapter_error_class.new("db down!") }
40
+ let(:test_double) { TestDouble.new(error, 1) }
41
+
42
+ before do
43
+ @logger = mock("Logger", :info => nil, :error => nil, :warn => nil)
44
+ Rapns::Daemon.stub(:logger).and_return(@logger)
45
+
46
+ ActiveRecord::Base.stub(:clear_all_connections!)
47
+ ActiveRecord::Base.stub(:establish_connection)
48
+ test_double.stub(:sleep)
49
+ end
50
+
51
+ it "should log the error raised" do
52
+ Rapns::Daemon.logger.should_receive(:error).with(error)
53
+ test_double.perform
54
+ end
55
+
56
+ it "should log that the database is being reconnected" do
57
+ Rapns::Daemon.logger.should_receive(:warn).with("Lost connection to database, reconnecting...")
58
+ test_double.perform
59
+ end
60
+
61
+ it "should log the reconnection attempt" do
62
+ Rapns::Daemon.logger.should_receive(:warn).with("Attempt 1")
63
+ test_double.perform
64
+ end
65
+
66
+ it "should clear all connections" do
67
+ ActiveRecord::Base.should_receive(:clear_all_connections!)
68
+ test_double.perform
69
+ end
70
+
71
+ it "should establish a new connection" do
72
+ ActiveRecord::Base.should_receive(:establish_connection)
73
+ test_double.perform
74
+ end
75
+
76
+ it "should test out the new connection by performing a count" do
77
+ Rapns::Notification.should_receive(:count)
78
+ test_double.perform
79
+ end
80
+
81
+ context "when the reconnection attempt is not successful" do
82
+ before do
83
+ class << Rapns::Notification
84
+ def count
85
+ @count_calls += 1
86
+ return if @count_calls == 2
87
+ raise @error
88
+ end
89
+ end
90
+ Rapns::Notification.instance_variable_set("@count_calls", 0)
91
+ Rapns::Notification.instance_variable_set("@error", error)
92
+ end
93
+
94
+ it "should log the 2nd attempt" do
95
+ Rapns::Daemon.logger.should_receive(:warn).with("Attempt 2")
96
+ test_double.perform
97
+ end
98
+
99
+ it "should log errors raised when the reconnection is not successful without notifying airbrake" do
100
+ Rapns::Daemon.logger.should_receive(:error).with(error, :airbrake_notify => false)
101
+ test_double.perform
102
+ end
103
+
104
+ it "should sleep to avoid thrashing when the database is down" do
105
+ test_double.should_receive(:sleep).with(2)
106
+ test_double.perform
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,13 @@
1
+ require "unit_spec_helper"
2
+
3
+ describe Rapns::DeliveryError do
4
+ let(:error) { Rapns::DeliveryError.new(4, 12, "Missing payload") }
5
+
6
+ it "returns an informative message" do
7
+ error.to_s.should == "Unable to deliver notification 12, received error 4 (Missing payload)"
8
+ end
9
+
10
+ it "returns the error code" do
11
+ error.code.should == 4
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ shared_examples_for 'an DeliveryHandler subclass' do
2
+ it 'logs all delivery errors' do
3
+ logger = stub
4
+ Rapns::Daemon.stub(:logger => logger)
5
+ error = StandardError.new
6
+ delivery_handler.stub(:deliver).and_raise(error)
7
+ Rapns::Daemon.logger.should_receive(:error).with(error)
8
+ delivery_handler.send(:handle_next_notification)
9
+ end
10
+
11
+ it "instructs the queue to wakeup the thread when told to stop" do
12
+ thread = stub(:join => nil)
13
+ Thread.stub(:new => thread)
14
+ queue.should_receive(:wakeup).with(thread)
15
+ delivery_handler.start
16
+ delivery_handler.stop
17
+ end
18
+
19
+ describe "when being stopped" do
20
+ before { queue.pop }
21
+
22
+ it "does not attempt to deliver a notification when a DeliveryQueue::::WakeupError is raised" do
23
+ queue.stub(:pop).and_raise(Rapns::Daemon::DeliveryQueue::WakeupError)
24
+ delivery_handler.should_not_receive(:deliver)
25
+ delivery_handler.send(:handle_next_notification)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ require "unit_spec_helper"
2
+
3
+ describe Rapns::Daemon::DeliveryQueue do
4
+ let(:queue) { Rapns::Daemon::DeliveryQueue.new }
5
+
6
+ it 'behaves likes a normal qeue' do
7
+ obj = stub
8
+ queue.push obj
9
+ queue.pop.should == obj
10
+ end
11
+
12
+ it 'returns false if notifications have not all been processed' do
13
+ queue.push stub
14
+ queue.notifications_processed?.should be_false
15
+ end
16
+
17
+ it 'returns false if the queue is empty but notifications have not all been processed' do
18
+ queue.push stub
19
+ queue.pop
20
+ queue.notifications_processed?.should be_false
21
+ end
22
+
23
+ it 'returns true if all notifications have been processed' do
24
+ queue.push stub
25
+ queue.pop
26
+ queue.notification_processed
27
+ queue.notifications_processed?.should be_true
28
+ end
29
+ end
@@ -0,0 +1,95 @@
1
+ require "unit_spec_helper"
2
+
3
+ describe Rapns::Daemon::Feeder do
4
+ let(:config) { stub(:batch_size => 5000) }
5
+ let!(:app) { Rapns::Apns::App.create!(:name => 'my_app', :environment => 'development', :certificate => TEST_CERT) }
6
+ let(:notification) { Rapns::Apns::Notification.create!(:device_token => "a" * 64, :app => app) }
7
+ let(:logger) { stub }
8
+
9
+ before do
10
+ Rapns::Daemon.stub(:logger => logger, :config => config)
11
+ Rapns::Daemon::Feeder.instance_variable_set("@stop", true)
12
+ Rapns::Daemon::AppRunner.stub(:idle => [stub(:app => app)])
13
+ end
14
+
15
+ def start
16
+ Rapns::Daemon::Feeder.start(0)
17
+ end
18
+
19
+ it "checks for new notifications with the ability to reconnect the database" do
20
+ Rapns::Daemon::Feeder.should_receive(:with_database_reconnect_and_retry)
21
+ start
22
+ end
23
+
24
+ it 'loads notifications in batches' do
25
+ relation = stub.as_null_object
26
+ relation.should_receive(:limit).with(5000)
27
+ Rapns::Notification.stub(:ready_for_delivery => relation)
28
+ start
29
+ end
30
+
31
+ it "enqueue the notification" do
32
+ notification.update_attributes!(:delivered => false)
33
+ Rapns::Daemon::AppRunner.should_receive(:enqueue).with(notification)
34
+ start
35
+ end
36
+
37
+ it 'does not enqueue the notification if the app runner is still processing the previous batch' do
38
+ Rapns::Daemon::AppRunner.should_not_receive(:enqueue)
39
+ start
40
+ end
41
+
42
+ it "enqueues an undelivered notification without deliver_after set" do
43
+ notification.update_attributes!(:delivered => false, :deliver_after => nil)
44
+ Rapns::Daemon::AppRunner.should_receive(:enqueue).with(notification)
45
+ start
46
+ end
47
+
48
+ it "enqueues a notification with a deliver_after time in the past" do
49
+ notification.update_attributes!(:delivered => false, :deliver_after => 1.hour.ago)
50
+ Rapns::Daemon::AppRunner.should_receive(:enqueue).with(notification)
51
+ start
52
+ end
53
+
54
+ it "does not enqueue a notification with a deliver_after time in the future" do
55
+ notification.update_attributes!(:delivered => false, :deliver_after => 1.hour.from_now)
56
+ Rapns::Daemon::AppRunner.should_not_receive(:enqueue)
57
+ start
58
+ end
59
+
60
+ it "does not enqueue a previously delivered notification" do
61
+ notification.update_attributes!(:delivered => true, :delivered_at => Time.now)
62
+ Rapns::Daemon::AppRunner.should_not_receive(:enqueue)
63
+ start
64
+ end
65
+
66
+ it "does not enqueue a notification that has previously failed delivery" do
67
+ notification.update_attributes!(:delivered => false, :failed => true)
68
+ Rapns::Daemon::AppRunner.should_not_receive(:enqueue)
69
+ start
70
+ end
71
+
72
+ it "logs errors" do
73
+ e = StandardError.new("bork")
74
+ Rapns::Notification.stub(:ready_for_delivery).and_raise(e)
75
+ Rapns::Daemon.logger.should_receive(:error).with(e)
76
+ start
77
+ end
78
+
79
+ it "interrupts sleep when stopped" do
80
+ Rapns::Daemon::Feeder.should_receive(:interrupt_sleep)
81
+ Rapns::Daemon::Feeder.stop
82
+ end
83
+
84
+ it "enqueues notifications when started" do
85
+ Rapns::Daemon::Feeder.should_receive(:enqueue_notifications).at_least(:once)
86
+ Rapns::Daemon::Feeder.stub(:loop).and_yield
87
+ start
88
+ end
89
+
90
+ it "sleeps for the given period" do
91
+ Rapns::Daemon::Feeder.should_receive(:interruptible_sleep).with(2)
92
+ Rapns::Daemon::Feeder.stub(:loop).and_yield
93
+ Rapns::Daemon::Feeder.start(2)
94
+ end
95
+ end
@@ -0,0 +1,17 @@
1
+ require 'unit_spec_helper'
2
+ require File.dirname(__FILE__) + '/../app_runner_shared.rb'
3
+
4
+ describe Rapns::Daemon::Gcm::AppRunner do
5
+ it_behaves_like 'an AppRunner subclass'
6
+
7
+ let(:app_class) { Rapns::Gcm::App }
8
+ let(:app) { app_class.new }
9
+ let(:runner) { Rapns::Daemon::Gcm::AppRunner.new(app) }
10
+ let(:handler) { stub(:start => nil, :stop => nil, :queue= => nil) }
11
+ let(:logger) { stub(:info => nil) }
12
+
13
+ before do
14
+ Rapns::Daemon.stub(:logger => logger)
15
+ Rapns::Daemon::Gcm::DeliveryHandler.stub(:new => handler)
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ require "unit_spec_helper"
2
+ require File.dirname(__FILE__) + '/../delivery_handler_shared.rb'
3
+
4
+ describe Rapns::Daemon::Gcm::DeliveryHandler do
5
+ it_should_behave_like 'an DeliveryHandler subclass'
6
+
7
+ let(:app) { stub }
8
+ let(:delivery_handler) { Rapns::Daemon::Gcm::DeliveryHandler.new(app) }
9
+ let(:notification) { stub }
10
+ let(:http) { stub(:shutdown => nil)}
11
+ let(:queue) { Rapns::Daemon::DeliveryQueue.new }
12
+
13
+ before do
14
+ Net::HTTP::Persistent.stub(:new => http)
15
+ Rapns::Daemon::Gcm::Delivery.stub(:perform)
16
+ delivery_handler.queue = queue
17
+ queue.push(notification)
18
+ end
19
+
20
+ it 'performs delivery of an notification' do
21
+ Rapns::Daemon::Gcm::Delivery.should_receive(:perform).with(app, http, notification)
22
+ delivery_handler.start
23
+ delivery_handler.stop
24
+ end
25
+
26
+ it 'initiates a persistent connection object' do
27
+ Net::HTTP::Persistent.should_receive(:new).with('rapns')
28
+ Rapns::Daemon::Gcm::DeliveryHandler.new(app)
29
+ end
30
+
31
+ it 'shuts down the http connection stopped' do
32
+ http.should_receive(:shutdown)
33
+ delivery_handler.start
34
+ delivery_handler.stop
35
+ end
36
+ end
@@ -0,0 +1,236 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Rapns::Daemon::Gcm::Delivery do
4
+ let(:app) { Rapns::Gcm::App.new(:name => 'MyApp', :auth_key => 'abc123') }
5
+ let(:notification) { Rapns::Gcm::Notification.create!(:app => app, :registration_ids => ['xyz']) }
6
+ let(:logger) { stub(:error => nil, :info => nil, :warn => nil) }
7
+ let(:response) { stub(:code => 200, :header => {}) }
8
+ let(:http) { stub(:shutdown => nil, :request => response)}
9
+ let(:now) { Time.parse('2012-10-14 00:00:00') }
10
+
11
+ def perform
12
+ Rapns::Daemon::Gcm::Delivery.perform(app, http, notification)
13
+ end
14
+
15
+ before do
16
+ Time.stub(:now => now)
17
+ Rapns::Daemon.stub(:logger => logger)
18
+ end
19
+
20
+ describe 'an 200 response' do
21
+ before do
22
+ response.stub(:code => 200)
23
+ end
24
+
25
+ it 'marks the notification as delivered if delivered successfully to all devices' do
26
+ response.stub(:body => JSON.dump({ 'failure' => 0 }))
27
+ expect do
28
+ perform
29
+ end.to change(notification, :delivered).to(true)
30
+ end
31
+
32
+ it 'logs that the notification was delivered' do
33
+ response.stub(:body => JSON.dump({ 'failure' => 0 }))
34
+ logger.should_receive(:info).with("[MyApp] 1 sent to xyz")
35
+ perform
36
+ end
37
+
38
+ it 'marks a notification as failed if any deliveries failed that cannot be retried.' do
39
+ body = {
40
+ 'failure' => 1,
41
+ 'success' => 1,
42
+ 'results' => [
43
+ { 'message_id' => '1:000' },
44
+ { 'error' => 'NotRegistered' }
45
+ ]}
46
+ response.stub(:body => JSON.dump(body))
47
+ perform rescue Rapns::DeliveryError
48
+ notification.reload
49
+ notification.failed.should be_true
50
+ notification.error_code = nil
51
+ notification.error_description = "Weee"
52
+ end
53
+
54
+ describe 'all deliveries returned Unavailable or InternalServerError' do
55
+ let(:body) {{
56
+ 'failure' => 2,
57
+ 'success' => 0,
58
+ 'results' => [
59
+ { 'error' => 'Unavailable' },
60
+ { 'error' => 'Unavailable' }
61
+ ]}}
62
+
63
+ before { response.stub(:body => JSON.dump(body)) }
64
+
65
+ it 'retries the notification respecting the Retry-After header' do
66
+ response.stub(:header => { 'retry-after' => 10 })
67
+ perform
68
+ notification.reload
69
+ notification.retries.should == 1
70
+ notification.deliver_after.should == now + 10.seconds
71
+ end
72
+
73
+ it 'retries the notification using exponential back-off if the Retry-After header is not present' do
74
+ notification.update_attribute(:retries, 8)
75
+ perform
76
+ notification.reload
77
+ notification.retries.should == 9
78
+ notification.deliver_after.should == now + 2 ** 9
79
+ end
80
+
81
+ it 'does not mark the notification as failed' do
82
+ expect do
83
+ perform
84
+ notification.reload
85
+ end.to_not change(notification, :failed).to(true)
86
+ end
87
+
88
+ it 'logs that the notification will be retried' do
89
+ Rapns::Daemon.logger.should_receive(:warn).with("All recipients unavailable. Notification #{notification.id} will be retired after 2012-10-14 00:00:02 (retry 1).")
90
+ perform
91
+ end
92
+ end
93
+
94
+ shared_examples_for 'an notification with some delivery failures' do
95
+ let(:new_notification) { Rapns::Gcm::Notification.where('id != ?', notification.id).first }
96
+
97
+ before { response.stub(:body => JSON.dump(body)) }
98
+
99
+ it 'marks the original notification as failed' do
100
+ perform rescue Rapns::DeliveryError
101
+ notification.reload
102
+ notification.failed.should be_true
103
+ notification.failed_at = now
104
+ notification.error_code.should be_nil
105
+ notification.error_description.should == error_description
106
+ end
107
+
108
+ it 'creates a new notification for the unavailable devices' do
109
+ notification.update_attributes(:registration_ids => ['id_0', 'id_1', 'id_2'], :data => {'one' => 1}, :collapse_key => 'thing', :delay_while_idle => true)
110
+ perform rescue Rapns::DeliveryError
111
+ new_notification.registration_ids.should == ['id_0', 'id_2']
112
+ new_notification.data.should == {'one' => 1}
113
+ new_notification.collapse_key.should == 'thing'
114
+ new_notification.delay_while_idle.should be_true
115
+ end
116
+
117
+ it 'sets the delivery time on the new notification to respect the Retry-After header' do
118
+ response.stub(:header => { 'retry-after' => 10 })
119
+ perform rescue Rapns::DeliveryError
120
+ new_notification.deliver_after.should == now + 10.seconds
121
+ end
122
+
123
+ it 'raises a DeliveryError' do
124
+ expect { perform }.to raise_error(Rapns::DeliveryError)
125
+ end
126
+ end
127
+
128
+ describe 'all deliveries failed with some as Unavailable or InternalServerError' do
129
+ let(:body) {{
130
+ 'failure' => 3,
131
+ 'success' => 0,
132
+ 'results' => [
133
+ { 'error' => 'Unavailable' },
134
+ { 'error' => 'NotRegistered' },
135
+ { 'error' => 'Unavailable' }
136
+ ]}}
137
+ let(:error_description) { "Failed to deliver to recipients 0, 1, 2. Errors: Unavailable, NotRegistered, Unavailable. 0, 2 will be retried as notification 2." }
138
+ it_should_behave_like 'an notification with some delivery failures'
139
+ end
140
+ end
141
+
142
+ describe 'some deliveries failed with Unavailable or InternalServerError' do
143
+ let(:body) {{
144
+ 'failure' => 2,
145
+ 'success' => 1,
146
+ 'results' => [
147
+ { 'error' => 'Unavailable' },
148
+ { 'message_id' => '1:000' },
149
+ { 'error' => 'InternalServerError' }
150
+ ]}}
151
+ let(:error_description) { "Failed to deliver to recipients 0, 2. Errors: Unavailable, InternalServerError. 0, 2 will be retried as notification 2." }
152
+ it_should_behave_like 'an notification with some delivery failures'
153
+ end
154
+
155
+ describe 'an 503 response' do
156
+ before { response.stub(:code => 503) }
157
+
158
+ it 'logs a warning that the notification will be retried.' do
159
+ logger.should_receive(:warn).with("GCM responded with an Service Unavailable Error. Notification 1 will be retired after 2012-10-14 00:00:02 (retry 1).")
160
+ perform
161
+ end
162
+
163
+ it 'respects an integer Retry-After header' do
164
+ response.stub(:header => { 'retry-after' => 10 })
165
+ expect do
166
+ perform
167
+ end.to change(notification, :deliver_after).to(now + 10)
168
+ end
169
+
170
+ it 'respects a HTTP-date Retry-After header' do
171
+ response.stub(:header => { 'retry-after' => 'Wed, 03 Oct 2012 20:55:11 GMT' })
172
+ expect do
173
+ perform
174
+ end.to change(notification, :deliver_after).to(Time.parse('Wed, 03 Oct 2012 20:55:11 GMT'))
175
+ end
176
+
177
+ it 'defaults to exponential back-off if the Retry-After header is not present' do
178
+ expect do
179
+ perform
180
+ end.to change(notification, :deliver_after).to(now + 2 ** 1)
181
+ end
182
+ end
183
+
184
+ describe 'an 500 response' do
185
+ before do
186
+ notification.update_attribute(:retries, 2)
187
+ response.stub(:code => 500)
188
+ end
189
+
190
+ it 'logs a warning that the notification has been re-queued.' do
191
+ Rapns::Daemon.logger.should_receive(:warn).with("GCM responded with an Internal Error. Notification #{notification.id} will be retired after #{(now + 2 ** 3).strftime("%Y-%m-%d %H:%M:%S")} (retry 3).")
192
+ perform
193
+ end
194
+
195
+ it 'sets deliver_after on the notification in accordance with the exponential back-off strategy.' do
196
+ expect do
197
+ perform
198
+ notification.reload
199
+ end.to change(notification, :deliver_after).to(now + 2 ** 3)
200
+ end
201
+ end
202
+
203
+ describe 'an 401 response' do
204
+ before { response.stub(:code => 401) }
205
+
206
+ it 'raises an error' do
207
+ expect { perform }.to raise_error(Rapns::DeliveryError)
208
+ end
209
+ end
210
+
211
+ describe 'an 400 response' do
212
+ before { response.stub(:code => 400) }
213
+
214
+ it 'marks the notification as failed' do
215
+ perform rescue Rapns::DeliveryError
216
+ notification.reload
217
+ notification.failed.should be_true
218
+ notification.failed_at.should == now
219
+ notification.error_code.should == 400
220
+ notification.error_description.should == 'GCM failed to parse the JSON request. Possibly an rapns bug, please open an issue.'
221
+ end
222
+ end
223
+
224
+ describe 'an un-handled response' do
225
+ before { response.stub(:code => 418) }
226
+
227
+ it 'marks the notification as failed' do
228
+ perform rescue Rapns::DeliveryError
229
+ notification.reload
230
+ notification.failed.should be_true
231
+ notification.failed_at.should == now
232
+ notification.error_code.should == 418
233
+ notification.error_description.should == "I'm a Teapot"
234
+ end
235
+ end
236
+ end