rpush 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +99 -0
  3. data/LICENSE +7 -0
  4. data/README.md +189 -0
  5. data/bin/rpush +36 -0
  6. data/config/database.yml +44 -0
  7. data/lib/generators/rpush_generator.rb +44 -0
  8. data/lib/generators/templates/add_adm.rb +23 -0
  9. data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
  10. data/lib/generators/templates/add_app_to_rapns.rb +11 -0
  11. data/lib/generators/templates/add_fail_after_to_rpush_notifications.rb +9 -0
  12. data/lib/generators/templates/add_gcm.rb +102 -0
  13. data/lib/generators/templates/add_rpush.rb +349 -0
  14. data/lib/generators/templates/add_wpns.rb +16 -0
  15. data/lib/generators/templates/create_rapns_apps.rb +16 -0
  16. data/lib/generators/templates/create_rapns_feedback.rb +18 -0
  17. data/lib/generators/templates/create_rapns_notifications.rb +29 -0
  18. data/lib/generators/templates/rename_rapns_to_rpush.rb +63 -0
  19. data/lib/generators/templates/rpush.rb +104 -0
  20. data/lib/rpush.rb +62 -0
  21. data/lib/rpush/TODO +3 -0
  22. data/lib/rpush/adm/app.rb +15 -0
  23. data/lib/rpush/adm/data_validator.rb +11 -0
  24. data/lib/rpush/adm/notification.rb +29 -0
  25. data/lib/rpush/apns/app.rb +29 -0
  26. data/lib/rpush/apns/binary_notification_validator.rb +12 -0
  27. data/lib/rpush/apns/device_token_format_validator.rb +12 -0
  28. data/lib/rpush/apns/feedback.rb +16 -0
  29. data/lib/rpush/apns/notification.rb +84 -0
  30. data/lib/rpush/apns_feedback.rb +13 -0
  31. data/lib/rpush/app.rb +18 -0
  32. data/lib/rpush/configuration.rb +75 -0
  33. data/lib/rpush/daemon.rb +140 -0
  34. data/lib/rpush/daemon/adm.rb +9 -0
  35. data/lib/rpush/daemon/adm/delivery.rb +222 -0
  36. data/lib/rpush/daemon/apns.rb +16 -0
  37. data/lib/rpush/daemon/apns/certificate_expired_error.rb +20 -0
  38. data/lib/rpush/daemon/apns/delivery.rb +64 -0
  39. data/lib/rpush/daemon/apns/disconnection_error.rb +20 -0
  40. data/lib/rpush/daemon/apns/feedback_receiver.rb +79 -0
  41. data/lib/rpush/daemon/app_runner.rb +187 -0
  42. data/lib/rpush/daemon/batch.rb +115 -0
  43. data/lib/rpush/daemon/constants.rb +59 -0
  44. data/lib/rpush/daemon/delivery.rb +28 -0
  45. data/lib/rpush/daemon/delivery_error.rb +19 -0
  46. data/lib/rpush/daemon/dispatcher/http.rb +21 -0
  47. data/lib/rpush/daemon/dispatcher/tcp.rb +30 -0
  48. data/lib/rpush/daemon/dispatcher_loop.rb +54 -0
  49. data/lib/rpush/daemon/dispatcher_loop_collection.rb +33 -0
  50. data/lib/rpush/daemon/feeder.rb +68 -0
  51. data/lib/rpush/daemon/gcm.rb +9 -0
  52. data/lib/rpush/daemon/gcm/delivery.rb +222 -0
  53. data/lib/rpush/daemon/interruptible_sleep.rb +61 -0
  54. data/lib/rpush/daemon/loggable.rb +31 -0
  55. data/lib/rpush/daemon/reflectable.rb +13 -0
  56. data/lib/rpush/daemon/retry_header_parser.rb +23 -0
  57. data/lib/rpush/daemon/retryable_error.rb +20 -0
  58. data/lib/rpush/daemon/service_config_methods.rb +33 -0
  59. data/lib/rpush/daemon/store/active_record.rb +154 -0
  60. data/lib/rpush/daemon/store/active_record/reconnectable.rb +68 -0
  61. data/lib/rpush/daemon/tcp_connection.rb +143 -0
  62. data/lib/rpush/daemon/too_many_requests_error.rb +20 -0
  63. data/lib/rpush/daemon/wpns.rb +9 -0
  64. data/lib/rpush/daemon/wpns/delivery.rb +132 -0
  65. data/lib/rpush/deprecatable.rb +23 -0
  66. data/lib/rpush/deprecation.rb +23 -0
  67. data/lib/rpush/embed.rb +28 -0
  68. data/lib/rpush/gcm/app.rb +11 -0
  69. data/lib/rpush/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
  70. data/lib/rpush/gcm/notification.rb +30 -0
  71. data/lib/rpush/logger.rb +63 -0
  72. data/lib/rpush/multi_json_helper.rb +16 -0
  73. data/lib/rpush/notification.rb +69 -0
  74. data/lib/rpush/notifier.rb +52 -0
  75. data/lib/rpush/payload_data_size_validator.rb +10 -0
  76. data/lib/rpush/push.rb +16 -0
  77. data/lib/rpush/railtie.rb +11 -0
  78. data/lib/rpush/reflection.rb +58 -0
  79. data/lib/rpush/registration_ids_count_validator.rb +10 -0
  80. data/lib/rpush/version.rb +3 -0
  81. data/lib/rpush/wpns/app.rb +9 -0
  82. data/lib/rpush/wpns/notification.rb +26 -0
  83. data/lib/tasks/cane.rake +18 -0
  84. data/lib/tasks/rpush.rake +16 -0
  85. data/lib/tasks/test.rake +38 -0
  86. data/spec/functional/adm_spec.rb +43 -0
  87. data/spec/functional/apns_spec.rb +58 -0
  88. data/spec/functional/embed_spec.rb +49 -0
  89. data/spec/functional/gcm_spec.rb +42 -0
  90. data/spec/functional/wpns_spec.rb +41 -0
  91. data/spec/support/cert_with_password.pem +90 -0
  92. data/spec/support/cert_without_password.pem +59 -0
  93. data/spec/support/install.sh +32 -0
  94. data/spec/support/simplecov_helper.rb +20 -0
  95. data/spec/support/simplecov_quality_formatter.rb +8 -0
  96. data/spec/tmp/.gitkeep +0 -0
  97. data/spec/unit/adm/app_spec.rb +58 -0
  98. data/spec/unit/adm/notification_spec.rb +45 -0
  99. data/spec/unit/apns/app_spec.rb +29 -0
  100. data/spec/unit/apns/feedback_spec.rb +9 -0
  101. data/spec/unit/apns/notification_spec.rb +208 -0
  102. data/spec/unit/apns_feedback_spec.rb +21 -0
  103. data/spec/unit/app_spec.rb +30 -0
  104. data/spec/unit/configuration_spec.rb +45 -0
  105. data/spec/unit/daemon/adm/delivery_spec.rb +243 -0
  106. data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
  107. data/spec/unit/daemon/apns/delivery_spec.rb +101 -0
  108. data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
  109. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +117 -0
  110. data/spec/unit/daemon/app_runner_spec.rb +292 -0
  111. data/spec/unit/daemon/batch_spec.rb +232 -0
  112. data/spec/unit/daemon/delivery_error_spec.rb +13 -0
  113. data/spec/unit/daemon/delivery_spec.rb +38 -0
  114. data/spec/unit/daemon/dispatcher/http_spec.rb +33 -0
  115. data/spec/unit/daemon/dispatcher/tcp_spec.rb +38 -0
  116. data/spec/unit/daemon/dispatcher_loop_collection_spec.rb +37 -0
  117. data/spec/unit/daemon/dispatcher_loop_spec.rb +71 -0
  118. data/spec/unit/daemon/feeder_spec.rb +98 -0
  119. data/spec/unit/daemon/gcm/delivery_spec.rb +310 -0
  120. data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
  121. data/spec/unit/daemon/reflectable_spec.rb +27 -0
  122. data/spec/unit/daemon/retryable_error_spec.rb +14 -0
  123. data/spec/unit/daemon/service_config_methods_spec.rb +33 -0
  124. data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
  125. data/spec/unit/daemon/store/active_record_spec.rb +357 -0
  126. data/spec/unit/daemon/tcp_connection_spec.rb +287 -0
  127. data/spec/unit/daemon/too_many_requests_error_spec.rb +14 -0
  128. data/spec/unit/daemon/wpns/delivery_spec.rb +159 -0
  129. data/spec/unit/daemon_spec.rb +159 -0
  130. data/spec/unit/deprecatable_spec.rb +32 -0
  131. data/spec/unit/deprecation_spec.rb +15 -0
  132. data/spec/unit/embed_spec.rb +50 -0
  133. data/spec/unit/gcm/app_spec.rb +4 -0
  134. data/spec/unit/gcm/notification_spec.rb +36 -0
  135. data/spec/unit/logger_spec.rb +127 -0
  136. data/spec/unit/notification_shared.rb +105 -0
  137. data/spec/unit/notification_spec.rb +15 -0
  138. data/spec/unit/notifier_spec.rb +49 -0
  139. data/spec/unit/push_spec.rb +43 -0
  140. data/spec/unit/reflection_spec.rb +30 -0
  141. data/spec/unit/rpush_spec.rb +9 -0
  142. data/spec/unit/wpns/app_spec.rb +4 -0
  143. data/spec/unit/wpns/notification_spec.rb +30 -0
  144. data/spec/unit_spec_helper.rb +101 -0
  145. metadata +276 -0
@@ -0,0 +1,13 @@
1
+ require "unit_spec_helper"
2
+
3
+ describe Rpush::DeliveryError do
4
+ let(:error) { Rpush::DeliveryError.new(4, 12, "Missing payload") }
5
+
6
+ it "returns an informative message" do
7
+ error.to_s.should eq "Unable to deliver notification 12, received error 4 (Missing payload)"
8
+ end
9
+
10
+ it "returns the error code" do
11
+ error.code.should eq 4
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Rpush::Daemon::Delivery do
4
+
5
+ class DeliverySpecDelivery < Rpush::Daemon::Delivery
6
+ def initialize(batch)
7
+ @batch = batch
8
+ end
9
+ end
10
+
11
+ let(:now) { Time.parse("2014-10-14 00:00:00") }
12
+ let(:batch) { double(Rpush::Daemon::Batch) }
13
+ let(:delivery) { DeliverySpecDelivery.new(batch) }
14
+ let(:notification) { Rpush::Apns::Notification.new }
15
+
16
+ before { Time.stub(now: now) }
17
+
18
+ describe 'mark_retryable' do
19
+
20
+ it 'does not retry a notification with an expired fail_after' do
21
+ batch.should_receive(:mark_failed).with(notification, nil, "Notification failed to be delivered before 2014-10-13 23:00:00.")
22
+ notification.fail_after = Time.now - 1.hour
23
+ delivery.mark_retryable(notification, Time.now + 1.hour)
24
+ end
25
+
26
+ it 'retries the notification if does not have a fail_after time' do
27
+ batch.should_receive(:mark_retryable)
28
+ notification.fail_after = nil
29
+ delivery.mark_retryable(notification, Time.now + 1.hour)
30
+ end
31
+
32
+ it 'retries the notification if the fail_after time has not been reached' do
33
+ batch.should_receive(:mark_retryable)
34
+ notification.fail_after = Time.now + 1.hour
35
+ delivery.mark_retryable(notification, Time.now + 1.hour)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Rpush::Daemon::Dispatcher::Http do
4
+ let(:app) { double }
5
+ let(:delivery_class) { double }
6
+ let(:notification) { double }
7
+ let(:batch) { double }
8
+ let(:http) { double }
9
+ let(:dispatcher) { Rpush::Daemon::Dispatcher::Http.new(app, delivery_class) }
10
+
11
+ before { Net::HTTP::Persistent.stub(:new => http) }
12
+
13
+ it 'constructs a new persistent connection' do
14
+ Net::HTTP::Persistent.should_receive(:new)
15
+ Rpush::Daemon::Dispatcher::Http.new(app, delivery_class)
16
+ end
17
+
18
+ describe 'dispatch' do
19
+ it 'delivers the notification' do
20
+ delivery = double
21
+ delivery_class.should_receive(:new).with(app, http, notification, batch).and_return(delivery)
22
+ delivery.should_receive(:perform)
23
+ dispatcher.dispatch(notification, batch)
24
+ end
25
+ end
26
+
27
+ describe 'cleanup' do
28
+ it 'closes the connection' do
29
+ http.should_receive(:shutdown)
30
+ dispatcher.cleanup
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Rpush::Daemon::Dispatcher::Tcp do
4
+ let(:app) { double }
5
+ let(:delivery) { double(:perform => nil) }
6
+ let(:delivery_class) { double(:new => delivery) }
7
+ let(:notification) { double }
8
+ let(:batch) { double }
9
+ let(:connection) { double(Rpush::Daemon::TcpConnection, :connect => nil) }
10
+ let(:host) { 'localhost' }
11
+ let(:port) { 1234 }
12
+ let(:host_proc) { Proc.new { |app| [host, port] } }
13
+ let(:dispatcher) { Rpush::Daemon::Dispatcher::Tcp.new(app, delivery_class, :host => host_proc) }
14
+
15
+ before { Rpush::Daemon::TcpConnection.stub(:new => connection) }
16
+
17
+ describe 'dispatch' do
18
+ it 'lazily connects the socket' do
19
+ Rpush::Daemon::TcpConnection.should_receive(:new).with(app, host, port).and_return(connection)
20
+ connection.should_receive(:connect)
21
+ dispatcher.dispatch(notification, batch)
22
+ end
23
+
24
+ it 'delivers the notification' do
25
+ delivery_class.should_receive(:new).with(app, connection, notification, batch).and_return(delivery)
26
+ delivery.should_receive(:perform)
27
+ dispatcher.dispatch(notification, batch)
28
+ end
29
+ end
30
+
31
+ describe 'cleanup' do
32
+ it 'closes the connection' do
33
+ dispatcher.dispatch(notification, batch) # lazily initialize connection
34
+ connection.should_receive(:close)
35
+ dispatcher.cleanup
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Rpush::Daemon::DispatcherLoopCollection do
4
+ let(:dispatcher_loop) { double.as_null_object }
5
+ let(:collection) { Rpush::Daemon::DispatcherLoopCollection.new }
6
+
7
+ it 'returns the size of the collection' do
8
+ collection.push(dispatcher_loop)
9
+ collection.size.should eq 1
10
+ end
11
+
12
+ it 'pops a dispatcher loop from the collection' do
13
+ collection.push(dispatcher_loop)
14
+ dispatcher_loop.should_receive(:stop)
15
+ dispatcher_loop.should_receive(:wakeup)
16
+ dispatcher_loop.should_receive(:wait)
17
+ collection.pop
18
+ collection.size.should eq 0
19
+ end
20
+
21
+ it 'wakes up all dispatcher loops when popping a single dispatcher_loop' do
22
+ collection.push(dispatcher_loop)
23
+ dispatcher_loop2 = double.as_null_object
24
+ collection.push(dispatcher_loop2)
25
+ dispatcher_loop.should_receive(:wakeup)
26
+ dispatcher_loop2.should_receive(:wakeup)
27
+ collection.pop
28
+ end
29
+
30
+ it 'stops all dispatcher detetcloops' do
31
+ collection.push(dispatcher_loop)
32
+ dispatcher_loop.should_receive(:stop)
33
+ dispatcher_loop.should_receive(:wakeup)
34
+ dispatcher_loop.should_receive(:wait)
35
+ collection.stop
36
+ end
37
+ end
@@ -0,0 +1,71 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Rpush::Daemon::DispatcherLoop do
4
+ def run_dispatcher_loop
5
+ dispatcher_loop.start
6
+ dispatcher_loop.stop
7
+ dispatcher_loop.wakeup
8
+ dispatcher_loop.wait
9
+ end
10
+
11
+ let(:notification) { double }
12
+ let(:batch) { double(:notification_dispatched => nil) }
13
+ let(:queue) { Queue.new }
14
+ let(:dispatcher) { double(:dispatch => nil, :cleanup => nil) }
15
+ let(:dispatcher_loop) { Rpush::Daemon::DispatcherLoop.new(queue, dispatcher) }
16
+ let(:store) { double(Rpush::Daemon::Store::ActiveRecord, release_connection: nil)}
17
+
18
+ before do
19
+ Rpush::Daemon.stub(:store => store)
20
+ queue.push([notification, batch])
21
+ end
22
+
23
+ it 'logs errors' do
24
+ logger = double
25
+ Rpush.stub(:logger => logger)
26
+ error = StandardError.new
27
+ dispatcher.stub(:dispatch).and_raise(error)
28
+ Rpush.logger.should_receive(:error).with(error)
29
+ run_dispatcher_loop
30
+ end
31
+
32
+ it 'reflects an exception' do
33
+ Rpush.stub(:logger => double(:error => nil))
34
+ error = StandardError.new
35
+ dispatcher.stub(:dispatch).and_raise(error)
36
+ dispatcher_loop.should_receive(:reflect).with(:error, error)
37
+ run_dispatcher_loop
38
+ end
39
+
40
+ it 'instructs the batch that the notification has been processed' do
41
+ batch.should_receive(:notification_dispatched)
42
+ run_dispatcher_loop
43
+ end
44
+
45
+ it "instructs the queue to wakeup the thread when told to stop" do
46
+ queue.should_receive(:push).with(Rpush::Daemon::DispatcherLoop::WAKEUP).and_call_original
47
+ run_dispatcher_loop
48
+ end
49
+
50
+ describe 'stop' do
51
+ before do
52
+ queue.clear
53
+ queue.push(Rpush::Daemon::DispatcherLoop::WAKEUP)
54
+ end
55
+
56
+ it 'does not attempt to dispatch when a WAKEUP is dequeued' do
57
+ dispatcher.should_not_receive(:dispatch)
58
+ run_dispatcher_loop
59
+ end
60
+
61
+ it 'instructs the dispatcher to cleanup' do
62
+ dispatcher.should_receive(:cleanup)
63
+ run_dispatcher_loop
64
+ end
65
+
66
+ it 'releases the store connection' do
67
+ Rpush::Daemon.store.should_receive(:release_connection)
68
+ run_dispatcher_loop
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,98 @@
1
+ require "unit_spec_helper"
2
+
3
+ describe Rpush::Daemon::Feeder do
4
+ let(:config) { double(:batch_size => 5000,
5
+ :push_poll => 0,
6
+ :embedded => false,
7
+ :push => false,
8
+ :wakeup => nil) }
9
+ let!(:app) { Rpush::Apns::App.create!(:name => 'my_app', :environment => 'development', :certificate => TEST_CERT) }
10
+ let(:notification) { Rpush::Apns::Notification.create!(:device_token => "a" * 64, :app => app) }
11
+ let(:logger) { double }
12
+ let(:interruptible_sleep) { double(:sleep => nil, :interrupt_sleep => nil) }
13
+ let(:store) { double(Rpush::Daemon::Store::ActiveRecord,
14
+ deliverable_notifications: [notification], release_connection: nil) }
15
+
16
+ before do
17
+ Rpush.stub(:config => config,:logger => logger)
18
+ Rpush::Daemon.stub(:store => store)
19
+ Rpush::Daemon::Feeder.stub(:stop? => true)
20
+ Rpush::Daemon::AppRunner.stub(:enqueue => nil, :idle => [double(:app => app)])
21
+ Rpush::Daemon::InterruptibleSleep.stub(:new => interruptible_sleep)
22
+ Rpush::Daemon::Feeder.instance_variable_set('@interruptible_sleeper', nil)
23
+ end
24
+
25
+ def start_and_stop
26
+ Rpush::Daemon::Feeder.start
27
+ Rpush::Daemon::Feeder.stop
28
+ end
29
+
30
+ it 'starts the loop in a new thread if embedded' do
31
+ config.stub(:embedded => true)
32
+ Thread.should_receive(:new).and_yield
33
+ Rpush::Daemon::Feeder.should_receive(:feed_forever)
34
+ start_and_stop
35
+ end
36
+
37
+ it 'loads deliverable notifications' do
38
+ Rpush::Daemon.store.should_receive(:deliverable_notifications).with([app])
39
+ start_and_stop
40
+ end
41
+
42
+ it 'does not attempt to load deliverable notifications if there are no idle runners' do
43
+ Rpush::Daemon::AppRunner.stub(:idle => [])
44
+ Rpush::Daemon.store.should_not_receive(:deliverable_notifications)
45
+ start_and_stop
46
+ end
47
+
48
+ it 'enqueues notifications without looping if in push mode' do
49
+ config.stub(:push => true)
50
+ Rpush::Daemon::Feeder.should_not_receive(:feed_forever)
51
+ Rpush::Daemon::Feeder.should_receive(:enqueue_notifications)
52
+ start_and_stop
53
+ end
54
+
55
+ it "enqueues the notifications" do
56
+ Rpush::Daemon::AppRunner.should_receive(:enqueue).with([notification])
57
+ start_and_stop
58
+ end
59
+
60
+ it "logs errors" do
61
+ e = StandardError.new("bork")
62
+ Rpush::Daemon.store.stub(:deliverable_notifications).and_raise(e)
63
+ Rpush.logger.should_receive(:error).with(e)
64
+ start_and_stop
65
+ end
66
+
67
+ describe 'stop' do
68
+ it 'interrupts sleep when stopped' do
69
+ Rpush::Daemon::Feeder.should_receive(:interrupt_sleep)
70
+ start_and_stop
71
+ end
72
+
73
+ it 'releases the store connection when stopped' do
74
+ Rpush::Daemon.store.should_receive(:release_connection)
75
+ start_and_stop
76
+ end
77
+ end
78
+
79
+ it "enqueues notifications when started" do
80
+ Rpush::Daemon::Feeder.should_receive(:enqueue_notifications).at_least(:once)
81
+ Rpush::Daemon::Feeder.stub(:loop).and_yield
82
+ start_and_stop
83
+ end
84
+
85
+ it "sleeps for the given period" do
86
+ config.stub(:push_poll => 2)
87
+ interruptible_sleep.should_receive(:sleep).with(2)
88
+ start_and_stop
89
+ end
90
+
91
+ it "creates the wakeup socket" do
92
+ bind = '127.0.0.1'
93
+ port = 12345
94
+ config.stub(:wakeup => { :bind => bind, :port => port})
95
+ interruptible_sleep.should_receive(:enable_wake_on_udp).with(bind, port)
96
+ start_and_stop
97
+ end
98
+ end
@@ -0,0 +1,310 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Rpush::Daemon::Gcm::Delivery do
4
+ let(:app) { Rpush::Gcm::App.new(:name => 'MyApp', :auth_key => 'abc123') }
5
+ let(:notification) { Rpush::Gcm::Notification.create!(:app => app, :registration_ids => ['xyz'], :deliver_after => Time.now) }
6
+ let(:logger) { double(:error => nil, :info => nil, :warn => nil) }
7
+ let(:response) { double(:code => 200, :header => {}) }
8
+ let(:http) { double(:shutdown => nil, :request => response)}
9
+ let(:now) { Time.parse('2012-10-14 00:00:00') }
10
+ let(:batch) { double(:mark_failed => nil, :mark_delivered => nil, :mark_retryable => nil) }
11
+ let(:delivery) { Rpush::Daemon::Gcm::Delivery.new(app, http, notification, batch) }
12
+ let(:store) { double(:create_gcm_notification => double(:id => 2)) }
13
+
14
+ def perform
15
+ delivery.perform
16
+ end
17
+
18
+ before do
19
+ delivery.stub(:reflect => nil)
20
+ Rpush::Daemon.stub(:store => store)
21
+ Time.stub(:now => now)
22
+ Rpush.stub(:logger => logger)
23
+ end
24
+
25
+ shared_examples_for 'a notification with some delivery failures' do
26
+ let(:new_notification) { Rpush::Gcm::Notification.where('id != ?', notification.id).first }
27
+
28
+ before { response.stub(:body => JSON.dump(body)) }
29
+
30
+ it 'marks the original notification as failed' do
31
+ delivery.should_receive(:mark_failed).with(nil, error_description)
32
+ perform rescue Rpush::DeliveryError
33
+ end
34
+
35
+ it 'creates a new notification for the unavailable devices' do
36
+ notification.update_attributes(:registration_ids => ['id_0', 'id_1', 'id_2'], :data => {'one' => 1}, :collapse_key => 'thing', :delay_while_idle => true)
37
+ response.stub(:header => { 'retry-after' => 10 })
38
+ attrs = { 'collapse_key' => 'thing', 'delay_while_idle' => true, 'app_id' => app.id }
39
+ store.should_receive(:create_gcm_notification).with(attrs, notification.data,
40
+ ['id_0', 'id_2'], now + 10.seconds, notification.app)
41
+ perform rescue Rpush::DeliveryError
42
+ end
43
+
44
+ it 'raises a DeliveryError' do
45
+ expect { perform }.to raise_error(Rpush::DeliveryError)
46
+ end
47
+ end
48
+
49
+ describe 'a 200 response' do
50
+ before do
51
+ response.stub(:code => 200)
52
+ end
53
+
54
+ it 'reflects on any IDs which successfully received the notification' do
55
+ body = {
56
+ 'failure' => 1,
57
+ 'success' => 1,
58
+ 'results' => [
59
+ { 'message_id' => '1:000' },
60
+ { 'error' => 'Err' }
61
+ ]
62
+ }
63
+
64
+ response.stub(:body => JSON.dump(body))
65
+ notification.stub(:registration_ids => ['1', '2'])
66
+ delivery.should_receive(:reflect).with(:gcm_delivered_to_recipient, notification, '1')
67
+ delivery.should_not_receive(:reflect).with(:gcm_delivered_to_recipient, notification, '2')
68
+ perform rescue Rpush::DeliveryError
69
+ end
70
+
71
+ it 'reflects on any IDs which failed to receive the notification' do
72
+ body = {
73
+ 'failure' => 1,
74
+ 'success' => 1,
75
+ 'results' => [
76
+ { 'error' => 'Err' },
77
+ { 'message_id' => '1:000' }
78
+ ]
79
+ }
80
+
81
+ response.stub(:body => JSON.dump(body))
82
+ notification.stub(:registration_ids => ['1', '2'])
83
+ delivery.should_receive(:reflect).with(:gcm_failed_to_recipient, notification, 'Err', '1')
84
+ delivery.should_not_receive(:reflect).with(:gcm_failed_to_recipient, notification, anything, '2')
85
+ perform rescue Rpush::DeliveryError
86
+ end
87
+
88
+ it 'reflects on canonical IDs' do
89
+ body = {
90
+ 'failure' => 0,
91
+ 'success' => 3,
92
+ 'canonical_ids' => 1,
93
+ 'results' => [
94
+ { 'message_id' => '1:000' },
95
+ { 'message_id' => '1:000', 'registration_id' => 'canonical123' },
96
+ { 'message_id' => '1:000' },
97
+ ]}
98
+
99
+ response.stub(:body => JSON.dump(body))
100
+ notification.stub(:registration_ids => ['1', '2', '3'])
101
+ delivery.should_receive(:reflect).with(:gcm_canonical_id, '2', 'canonical123')
102
+ perform
103
+ end
104
+
105
+ it 'reflects on invalid IDs' do
106
+ body = {
107
+ 'failure' => 1,
108
+ 'success' => 2,
109
+ 'canonical_ids' => 0,
110
+ 'results' => [
111
+ { 'message_id' => '1:000' },
112
+ { 'error' => 'NotRegistered' },
113
+ { 'message_id' => '1:000' },
114
+ ]}
115
+
116
+ response.stub(:body => JSON.dump(body))
117
+ notification.stub(:registration_ids => ['1', '2', '3'])
118
+ delivery.should_receive(:reflect).with(:gcm_invalid_registration_id, app, 'NotRegistered', '2')
119
+ perform rescue Rpush::DeliveryError
120
+ end
121
+
122
+ describe 'when delivered successfully to all devices' do
123
+ let(:body) {{
124
+ 'failure' => 0,
125
+ 'success' => 1,
126
+ 'results' => [{ 'message_id' => '1:000'}]
127
+ }}
128
+
129
+ before { response.stub(:body => JSON.dump(body)) }
130
+
131
+ it 'marks the notification as delivered' do
132
+ delivery.should_receive(:mark_delivered)
133
+ perform
134
+ end
135
+
136
+ it 'logs that the notification was delivered' do
137
+ logger.should_receive(:info).with("[MyApp] #{notification.id} sent to xyz")
138
+ perform
139
+ end
140
+ end
141
+
142
+ it 'marks a notification as failed if any ids are invalid' do
143
+ body = {
144
+ 'failure' => 1,
145
+ 'success' => 2,
146
+ 'canonical_ids' => 0,
147
+ 'results' => [
148
+ { 'message_id' => '1:000' },
149
+ { 'error' => 'NotRegistered' },
150
+ { 'message_id' => '1:000' },
151
+ ]}
152
+
153
+ response.stub(:body => JSON.dump(body))
154
+ delivery.should_receive(:mark_failed)
155
+ delivery.should_not_receive(:mark_retryable)
156
+ store.should_not_receive(:create_gcm_notification)
157
+ perform rescue Rpush::DeliveryError
158
+ end
159
+
160
+ it 'marks a notification as failed if any deliveries failed that cannot be retried' do
161
+ body = {
162
+ 'failure' => 1,
163
+ 'success' => 1,
164
+ 'results' => [
165
+ { 'message_id' => '1:000' },
166
+ { 'error' => 'InvalidDataKey' }
167
+ ]}
168
+ response.stub(:body => JSON.dump(body))
169
+ delivery.should_receive(:mark_failed).with(nil, "Failed to deliver to all recipients. Errors: InvalidDataKey.")
170
+ perform rescue Rpush::DeliveryError
171
+ end
172
+
173
+ describe 'all deliveries failed with Unavailable or InternalServerError' do
174
+ let(:body) {{
175
+ 'failure' => 2,
176
+ 'success' => 0,
177
+ 'results' => [
178
+ { 'error' => 'Unavailable' },
179
+ { 'error' => 'Unavailable' }
180
+ ]}}
181
+
182
+ before do
183
+ response.stub(:body => JSON.dump(body))
184
+ notification.stub(:registration_ids => ['1', '2'])
185
+ end
186
+
187
+ it 'retries the notification respecting the Retry-After header' do
188
+ response.stub(:header => { 'retry-after' => 10 })
189
+ delivery.should_receive(:mark_retryable).with(notification, now + 10.seconds)
190
+ perform
191
+ end
192
+
193
+ it 'retries the notification using exponential back-off if the Retry-After header is not present' do
194
+ delivery.should_receive(:mark_retryable).with(notification, now + 2)
195
+ perform
196
+ end
197
+
198
+ it 'does not mark the notification as failed' do
199
+ delivery.should_not_receive(:mark_failed)
200
+ perform
201
+ end
202
+
203
+ it 'logs that the notification will be retried' do
204
+ notification.retries = 1
205
+ notification.deliver_after = now + 2
206
+ Rpush.logger.should_receive(:warn).with("[MyApp] All recipients unavailable. Notification #{notification.id} will be retried after 2012-10-14 00:00:02 (retry 1).")
207
+ perform
208
+ end
209
+ end
210
+
211
+ describe 'all deliveries failed with some as Unavailable or InternalServerError' do
212
+ let(:body) {{
213
+ 'failure' => 3,
214
+ 'success' => 0,
215
+ 'results' => [
216
+ { 'error' => 'Unavailable' },
217
+ { 'error' => 'InvalidDataKey' },
218
+ { 'error' => 'Unavailable' }
219
+ ]}}
220
+ let(:error_description) { /#{Regexp.escape("Failed to deliver to recipients 0, 1, 2. Errors: Unavailable, InvalidDataKey, Unavailable. 0, 2 will be retried as notification")} [\d]+\./ }
221
+ it_should_behave_like 'a notification with some delivery failures'
222
+ end
223
+
224
+ describe 'some deliveries failed with Unavailable or InternalServerError' do
225
+ let(:body) {{
226
+ 'failure' => 2,
227
+ 'success' => 1,
228
+ 'results' => [
229
+ { 'error' => 'Unavailable' },
230
+ { 'message_id' => '1:000' },
231
+ { 'error' => 'InternalServerError' }
232
+ ]}}
233
+ let(:error_description) { /#{Regexp.escape("Failed to deliver to recipients 0, 2. Errors: Unavailable, InternalServerError. 0, 2 will be retried as notification")} [\d]+\./ }
234
+ it_should_behave_like 'a notification with some delivery failures'
235
+ end
236
+ end
237
+
238
+ describe 'a 503 response' do
239
+ before { response.stub(:code => 503) }
240
+
241
+ it 'logs a warning that the notification will be retried.' do
242
+ notification.retries = 1
243
+ notification.deliver_after = now + 2
244
+ logger.should_receive(:warn).with("[MyApp] GCM responded with an Service Unavailable Error. Notification #{notification.id} will be retried after 2012-10-14 00:00:02 (retry 1).")
245
+ perform
246
+ end
247
+
248
+ it 'respects an integer Retry-After header' do
249
+ response.stub(:header => { 'retry-after' => 10 })
250
+ delivery.should_receive(:mark_retryable).with(notification, now + 10.seconds)
251
+ perform
252
+ end
253
+
254
+ it 'respects a HTTP-date Retry-After header' do
255
+ response.stub(:header => { 'retry-after' => 'Wed, 03 Oct 2012 20:55:11 GMT' })
256
+ delivery.should_receive(:mark_retryable).with(notification, Time.parse('Wed, 03 Oct 2012 20:55:11 GMT'))
257
+ perform
258
+ end
259
+
260
+ it 'defaults to exponential back-off if the Retry-After header is not present' do
261
+ delivery.should_receive(:mark_retryable).with(notification, now + 2 ** 1)
262
+ perform
263
+ end
264
+ end
265
+
266
+ describe 'a 500 response' do
267
+ before do
268
+ notification.update_attribute(:retries, 2)
269
+ response.stub(:code => 500)
270
+ end
271
+
272
+ it 'logs a warning that the notification has been re-queued.' do
273
+ notification.retries = 3
274
+ notification.deliver_after = now + 2 ** 3
275
+ Rpush.logger.should_receive(:warn).with("[MyApp] GCM responded with an Internal Error. Notification #{notification.id} will be retried after #{(now + 2 ** 3).strftime("%Y-%m-%d %H:%M:%S")} (retry 3).")
276
+ perform
277
+ end
278
+
279
+ it 'retries the notification in accordance with the exponential back-off strategy.' do
280
+ delivery.should_receive(:mark_retryable).with(notification, now + 2 ** 3)
281
+ perform
282
+ end
283
+ end
284
+
285
+ describe 'a 401 response' do
286
+ before { response.stub(:code => 401) }
287
+
288
+ it 'raises an error' do
289
+ expect { perform }.to raise_error(Rpush::DeliveryError)
290
+ end
291
+ end
292
+
293
+ describe 'a 400 response' do
294
+ before { response.stub(:code => 400) }
295
+
296
+ it 'marks the notification as failed' do
297
+ delivery.should_receive(:mark_failed).with(400, 'GCM failed to parse the JSON request. Possibly an Rpush bug, please open an issue.')
298
+ perform rescue Rpush::DeliveryError
299
+ end
300
+ end
301
+
302
+ describe 'an un-handled response' do
303
+ before { response.stub(:code => 418) }
304
+
305
+ it 'marks the notification as failed' do
306
+ delivery.should_receive(:mark_failed).with(418, "I'm a Teapot")
307
+ perform rescue Rpush::DeliveryError
308
+ end
309
+ end
310
+ end