rpush 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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