rapns 3.0.0-java

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