ruby-push-notifications 0.0.1

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 (39) hide show
  1. data/.gitignore +35 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +55 -0
  6. data/LICENSE +22 -0
  7. data/README.md +56 -0
  8. data/Rakefile +12 -0
  9. data/examples/apns copy.rb +17 -0
  10. data/examples/apns.rb +16 -0
  11. data/examples/gcm copy.rb +16 -0
  12. data/examples/gcm.rb +16 -0
  13. data/lib/ruby-push-notifications.rb +8 -0
  14. data/lib/ruby-push-notifications/apns.rb +19 -0
  15. data/lib/ruby-push-notifications/apns/apns_connection.rb +45 -0
  16. data/lib/ruby-push-notifications/apns/apns_notification.rb +62 -0
  17. data/lib/ruby-push-notifications/apns/apns_pusher.rb +51 -0
  18. data/lib/ruby-push-notifications/gcm.rb +7 -0
  19. data/lib/ruby-push-notifications/gcm/gcm_connection.rb +31 -0
  20. data/lib/ruby-push-notifications/gcm/gcm_error.rb +14 -0
  21. data/lib/ruby-push-notifications/gcm/gcm_notification.rb +21 -0
  22. data/lib/ruby-push-notifications/gcm/gcm_pusher.rb +17 -0
  23. data/lib/ruby-push-notifications/gcm/gcm_response.rb +52 -0
  24. data/lib/ruby-push-notifications/gcm/gcm_result.rb +38 -0
  25. data/lib/ruby-push-notifications/version.rb +3 -0
  26. data/ruby-push-notifications.gemspec +26 -0
  27. data/spec/factories.rb +10 -0
  28. data/spec/factories/notifications.rb +15 -0
  29. data/spec/ruby-push-notifications/apns/apns_connection_spec.rb +79 -0
  30. data/spec/ruby-push-notifications/apns/apns_notification_spec.rb +30 -0
  31. data/spec/ruby-push-notifications/apns/apns_pusher_spec.rb +299 -0
  32. data/spec/ruby-push-notifications/gcm/gcm_connection_spec.rb +32 -0
  33. data/spec/ruby-push-notifications/gcm/gcm_notification_spec.rb +24 -0
  34. data/spec/ruby-push-notifications/gcm/gcm_pusher_spec.rb +45 -0
  35. data/spec/ruby-push-notifications/gcm/gcm_response_spec.rb +82 -0
  36. data/spec/spec_helper.rb +110 -0
  37. data/spec/support/dummy.pem +44 -0
  38. data/spec/support/factory_girl.rb +5 -0
  39. metadata +177 -0
@@ -0,0 +1,7 @@
1
+ require 'ruby-push-notifications/gcm/gcm_connection'
2
+ require 'ruby-push-notifications/gcm/gcm_notification'
3
+ require 'ruby-push-notifications/gcm/gcm_pusher'
4
+ require 'ruby-push-notifications/gcm/gcm_response'
5
+ require 'ruby-push-notifications/gcm/gcm_error'
6
+ require 'ruby-push-notifications/gcm/gcm_result'
7
+
@@ -0,0 +1,31 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ module RubyPushNotifications
5
+ module GCM
6
+ class GCMConnection
7
+
8
+ GCM_URL = 'https://android.googleapis.com/gcm/send'
9
+
10
+ CONTENT_TYPE_HEADER = 'Content-Type'
11
+ JSON_CONTENT_TYPE = 'application/json'
12
+ AUTHORIZATION_HEADER = 'Authorization'
13
+
14
+ def self.post(notification, key)
15
+ headers = {
16
+ CONTENT_TYPE_HEADER => JSON_CONTENT_TYPE,
17
+ AUTHORIZATION_HEADER => "key=#{key}"
18
+ }
19
+
20
+ url = URI.parse GCM_URL
21
+ http = Net::HTTP.new url.host, url.port
22
+ http.use_ssl = true
23
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
24
+
25
+ response = http.post url.path, notification, headers
26
+
27
+ GCMResponse.new response.code.to_i, response.body
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+
2
+ module RubyPushNotifications
3
+ module GCM
4
+ class GCMError < StandardError ; end
5
+
6
+ class MalformedGCMJSONError < GCMError ; end
7
+
8
+ class GCMAuthError < GCMError ; end
9
+
10
+ class GCMInternalError < GCMError ; end
11
+
12
+ class UnexpectedGCMResponseError < GCMError ; end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+
2
+ module RubyPushNotifications
3
+ module GCM
4
+ class GCMNotification
5
+
6
+ attr_accessor :results
7
+
8
+ def initialize(registration_ids, data)
9
+ @registration_ids = registration_ids
10
+ @data = data
11
+ end
12
+
13
+ def as_gcm_json
14
+ JSON.dump(
15
+ registration_ids: @registration_ids,
16
+ data: @data
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+
2
+ module RubyPushNotifications
3
+ module GCM
4
+ class GCMPusher
5
+
6
+ def initialize(key)
7
+ @key = key
8
+ end
9
+
10
+ def push(notifications)
11
+ notifications.each do |notif|
12
+ notif.results = GCMConnection.post notif.as_gcm_json, @key
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+
2
+ module RubyPushNotifications
3
+ module GCM
4
+ class GCMResponse
5
+
6
+ attr_reader :success, :failed, :canonical_ids, :results
7
+
8
+ def initialize(code, body)
9
+ case code
10
+ when 200
11
+ parse_response body
12
+ when 400
13
+ raise MalformedGCMJSONError, body
14
+ when 401
15
+ raise GCMAuthError, body
16
+ when 500..599
17
+ raise GCMInternalError, body
18
+ else
19
+ raise UnexpectedGCMResponseError, code
20
+ end
21
+ end
22
+
23
+ def ==(other)
24
+ (other.is_a?(GCMResponse) &&
25
+ success == other.success &&
26
+ failed == other.failed &&
27
+ canonical_ids == other.canonical_ids &&
28
+ results == other.results) || super(other)
29
+ end
30
+
31
+ private
32
+
33
+ def parse_response(body)
34
+ json = JSON.parse body, symbolize_names: true
35
+ @success = json[:success]
36
+ @failed = json[:failure]
37
+ @canonical_ids = json[:canonical_ids]
38
+ @results = (json[:results] || []).map { |result| gcm_result_for result }
39
+ end
40
+
41
+ def gcm_result_for(result)
42
+ if canonical_id = result[:registration_id]
43
+ GCMCanonicalIDResult.new canonical_id
44
+ elsif error = result[:error]
45
+ GCMResultError.new error
46
+ else
47
+ GCMResultOK.new
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,38 @@
1
+
2
+ module RubyPushNotifications
3
+ module GCM
4
+ class GCMResult ; end
5
+
6
+ class GCMResultOK < GCMResult
7
+
8
+ def ==(other)
9
+ other.is_a?(GCMResultOK) || super(other)
10
+ end
11
+ end
12
+
13
+ class GCMCanonicalIDResult < GCMResult
14
+ attr_reader :canonical_id
15
+
16
+ def initialize(canonical_id)
17
+ @canonical_id = canonical_id
18
+ end
19
+
20
+ def ==(other)
21
+ (other.is_a?(GCMCanonicalIDResult) && @canonical_id == other.canonical_id) ||
22
+ super(other)
23
+ end
24
+ end
25
+
26
+ class GCMResultError < GCMResult
27
+ attr_accessor :error
28
+
29
+ def initialize(error)
30
+ @error = error
31
+ end
32
+
33
+ def ==(other)
34
+ (other.is_a?(GCMResultError) && @error == other.error) || super(other)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module RubyPushNotifications
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ruby-push-notifications/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ruby-push-notifications"
8
+ spec.version = RubyPushNotifications::VERSION
9
+ spec.authors = ['Carlos Alonso']
10
+ spec.email = ['info@mrcalonso.com']
11
+ spec.summary = %q{iOS and Android Push Notifications made easy!}
12
+ spec.description = %q{Easy to use gem to send iOS and Android Push notifications}
13
+ spec.homepage = 'https://github.com/calonso/ruby-push-notifications'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.6'
22
+ spec.add_development_dependency 'rake', '~> 10.4'
23
+ spec.add_development_dependency 'rspec', '~> 3.2'
24
+ spec.add_development_dependency 'factory_girl', '~> 4.0'
25
+ spec.add_development_dependency 'webmock', '~> 1.20'
26
+ end
data/spec/factories.rb ADDED
@@ -0,0 +1,10 @@
1
+
2
+ FactoryGirl.define do
3
+ sequence :apns_token do |i|
4
+ "ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b596#{i}"
5
+ end
6
+
7
+ sequence :gcm_registration_id do |i|
8
+ "APA91bHPRgkF3JUikC4ENAHEeMrd41Zxv3hVZjC9KtT8OvPVGJ-hQMRKRrZuJAEcl7B338qju59zJMjw2DELjzEvxwYv7hH5Ynpc1ODQ0aT4U4OFEeco8ohsN5PjL1iC2dNtk2BAokeMCg2ZXKqpc8FXKmhX94kIxQ#{i}"
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ FactoryGirl.define do
2
+ factory :apns_notification, class: 'RubyPushNotifications::APNS::APNSNotification' do
3
+ tokens { [generate(:apns_token)] }
4
+ data a: 1
5
+
6
+ initialize_with { new tokens, data }
7
+ end
8
+
9
+ factory :gcm_notification, class: 'RubyPushNotifications::GCM::GCMNotification' do
10
+ registration_ids { [generate(:gcm_registration_id)] }
11
+ data a: 1
12
+
13
+ initialize_with { new registration_ids, data }
14
+ end
15
+ end
@@ -0,0 +1,79 @@
1
+
2
+ module RubyPushNotifications
3
+ module APNS
4
+ describe APNSConnection do
5
+
6
+ let(:cert) { File.read 'spec/support/dummy.pem' }
7
+ let(:tcp_socket) { instance_double(TCPSocket).as_null_object }
8
+ let(:ssl_socket) { instance_double(OpenSSL::SSL::SSLSocket).as_null_object }
9
+
10
+ describe '::open' do
11
+ before do
12
+ allow(TCPSocket).to receive(:new).with('gateway.sandbox.push.apple.com', 2195).and_return tcp_socket
13
+ allow(OpenSSL::SSL::SSLSocket).to receive(:new).with(tcp_socket, an_instance_of(OpenSSL::SSL::SSLContext)).and_return ssl_socket
14
+ end
15
+
16
+ it 'creates the connection' do
17
+ expect(TCPSocket).to receive(:new).with('gateway.sandbox.push.apple.com', 2195).and_return tcp_socket
18
+ expect(OpenSSL::SSL::SSLSocket).to receive(:new).with(tcp_socket, an_instance_of(OpenSSL::SSL::SSLContext)).and_return ssl_socket
19
+ APNSConnection.open cert, true
20
+ end
21
+
22
+ it 'returns an instance of APNSConnection' do
23
+ expect(APNSConnection.open cert, true).to be_a APNSConnection
24
+ end
25
+ end
26
+
27
+ describe '#close' do
28
+ let(:connection) { APNSConnection.new tcp_socket, ssl_socket }
29
+
30
+ it 'closes the ssl socket' do
31
+ expect(ssl_socket).to receive(:close)
32
+ connection.close
33
+ end
34
+
35
+ it 'closes the tcp socket' do
36
+ expect(tcp_socket).to receive(:close)
37
+ connection.close
38
+ end
39
+ end
40
+
41
+ describe '#write' do
42
+ let(:connection) { APNSConnection.new tcp_socket, ssl_socket }
43
+ let(:contents_string) { 'the contents string' }
44
+
45
+ it 'writes the ssl socket' do
46
+ expect(ssl_socket).to receive(:write).with contents_string
47
+ connection.write contents_string
48
+ end
49
+ end
50
+
51
+ describe '#read' do
52
+ let(:connection) { APNSConnection.new tcp_socket, ssl_socket }
53
+
54
+ it 'writes the ssl socket' do
55
+ expect(ssl_socket).to receive(:read).with 6
56
+ connection.read 6
57
+ end
58
+ end
59
+
60
+ describe '#flush' do
61
+ let(:connection) { APNSConnection.new tcp_socket, ssl_socket }
62
+
63
+ it 'flushes the ssl socket' do
64
+ expect(ssl_socket).to receive :flush
65
+ connection.flush
66
+ end
67
+ end
68
+
69
+ describe 'IO behavior' do
70
+ let(:connection) { APNSConnection.new tcp_socket, ssl_socket }
71
+
72
+ it 'can be selected' do
73
+ allow(ssl_socket).to receive(:to_io).and_return IO.new(IO.sysopen('/dev/null'))
74
+ IO.select [connection]
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,30 @@
1
+
2
+ module RubyPushNotifications
3
+ module APNS
4
+ describe APNSNotification do
5
+ describe 'building messages for each token' do
6
+
7
+ let(:device_tokens) { ['12', '34'] }
8
+ let(:data) { { a: 1 } }
9
+ let(:notification) { APNSNotification.new device_tokens, data }
10
+ let(:notif_id) { 1 }
11
+
12
+ it 'successfully creates the apns binary' do
13
+ json = JSON.dump data
14
+ expect do |b|
15
+ notification.each_message notif_id, &b
16
+ end.to yield_successive_args apns_binary(json, device_tokens[0], notif_id), apns_binary(json, device_tokens[1], notif_id+1)
17
+ end
18
+
19
+ it 'caches the payload' do
20
+ expect(JSON).to receive(:dump).with(data).once.and_return 'dummy string'
21
+ notification.each_message(notif_id) { }
22
+ end
23
+
24
+ it 'validates the data'
25
+
26
+ it 'validates the tokens'
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,299 @@
1
+
2
+ module RubyPushNotifications
3
+ module APNS
4
+ describe APNSPusher do
5
+
6
+ let(:sandbox) { true }
7
+ let(:certificate) { 'abc' }
8
+ let(:pusher) { APNSPusher.new certificate, sandbox }
9
+ let(:connection) { instance_double(APNSConnection).as_null_object }
10
+ let(:data) { { a: 1 } }
11
+
12
+ before do
13
+ allow(APNSConnection).to receive(:open).with(certificate, sandbox).and_return connection
14
+ end
15
+
16
+ describe '#push' do
17
+
18
+ context 'a single notification' do
19
+
20
+ context 'containing a single destination' do
21
+
22
+ let(:token) { generate :apns_token }
23
+ let(:notification) { build :apns_notification, data: data, tokens: [token] }
24
+
25
+ describe 'success' do
26
+
27
+ before { allow(IO).to receive(:select).and_return [] }
28
+
29
+ it 'writes the notification to the socket' do
30
+ expect(connection).to receive(:write).with apns_binary(data, token, 0)
31
+ pusher.push [notification]
32
+ end
33
+
34
+ it 'flushes the socket contents' do
35
+ expect(connection).to receive(:flush)
36
+ pusher.push [notification]
37
+ end
38
+
39
+ it 'saves the results into the notification' do
40
+ expect do
41
+ pusher.push [notification]
42
+ end.to change { notification.results }.from(nil).to [NO_ERROR_STATUS_CODE]
43
+ end
44
+ end
45
+
46
+ describe 'failure' do
47
+
48
+ before do
49
+ allow(IO).to receive(:select).and_return [[connection]]
50
+ allow(connection).to receive(:read).with(6).and_return [8, PROCESSING_ERROR_STATUS_CODE, 0].pack 'ccN'
51
+ end
52
+
53
+ it 'does not retry' do
54
+ expect(connection).to receive(:write).with(apns_binary(data, token, 0)).once
55
+ pusher.push [notification]
56
+ end
57
+
58
+ it 'saves the error' do
59
+ expect do
60
+ pusher.push [notification]
61
+ end.to change { notification.results }.from(nil).to [PROCESSING_ERROR_STATUS_CODE]
62
+ end
63
+ end
64
+ end
65
+
66
+ context 'containing several destinations' do
67
+ let(:tokens) { [generate(:apns_token), generate(:apns_token)] }
68
+ let(:notification) { build :apns_notification, data: data, tokens: tokens }
69
+
70
+ describe 'success' do
71
+
72
+ before { allow(IO).to receive(:select).and_return [] }
73
+
74
+ it 'writes the messages to the socket' do
75
+ expect(connection).to receive(:write).with apns_binary(data, tokens[0], 0)
76
+ expect(connection).to receive(:write).with apns_binary(data, tokens[1], 1)
77
+ pusher.push [notification]
78
+ end
79
+
80
+ it 'flushes the socket contents' do
81
+ expect(connection).to receive(:flush).once
82
+ pusher.push [notification]
83
+ end
84
+
85
+ it 'saves the results into the notification' do
86
+ expect do
87
+ pusher.push [notification]
88
+ end.to change { notification.results }.from(nil).to [NO_ERROR_STATUS_CODE, NO_ERROR_STATUS_CODE]
89
+ end
90
+ end
91
+
92
+ describe 'failure' do
93
+
94
+ let(:connection2) { instance_double(APNSConnection).as_null_object }
95
+
96
+ before do
97
+ allow(APNSConnection).to receive(:open).with(certificate, sandbox).and_return connection, connection2
98
+ end
99
+
100
+ context 'failing first' do
101
+ before do
102
+ allow(IO).to receive(:select).and_return [[connection]], []
103
+ allow(connection).to receive(:read).with(6).and_return [8, PROCESSING_ERROR_STATUS_CODE, 0].pack 'ccN'
104
+ end
105
+
106
+ it 'sends the each notification once and to the expected connection instance' do
107
+ expect(connection).to receive(:write).with(apns_binary(data, tokens[0], 0)).once
108
+ expect(connection2).to receive(:write).with(apns_binary(data, tokens[1], 1)).once
109
+ pusher.push [notification]
110
+ end
111
+
112
+ it 'stores the error' do
113
+ expect do
114
+ pusher.push [notification]
115
+ end.to change { notification.results }.from(nil).to [PROCESSING_ERROR_STATUS_CODE, NO_ERROR_STATUS_CODE]
116
+ end
117
+ end
118
+
119
+ context 'failing first but delayed error' do
120
+ before do
121
+ allow(IO).to receive(:select).and_return [], [[connection]], []
122
+ allow(connection).to receive(:read).with(6).and_return [8, PROCESSING_ERROR_STATUS_CODE, 0].pack 'ccN'
123
+ end
124
+
125
+ it 'sends the second notification twice' do
126
+ expect(connection).to receive(:write).with(apns_binary(data, tokens[0], 0)).once
127
+ expect(connection).to receive(:write).with(apns_binary(data, tokens[1], 1)).once
128
+ expect(connection2).to receive(:write).with(apns_binary(data, tokens[1], 1)).once
129
+ pusher.push [notification]
130
+ end
131
+
132
+ it 'stores the error' do
133
+ expect do
134
+ pusher.push [notification]
135
+ end.to change { notification.results }.from(nil).to [PROCESSING_ERROR_STATUS_CODE, NO_ERROR_STATUS_CODE]
136
+ end
137
+ end
138
+
139
+ context 'failing last' do
140
+ before do
141
+ allow(IO).to receive(:select).and_return [], [[connection]]
142
+ allow(connection).to receive(:read).with(6).and_return [8, PROCESSING_ERROR_STATUS_CODE, 1].pack 'ccN'
143
+ end
144
+
145
+ it 'sends the second notification just once' do
146
+ expect(connection).to receive(:write).with(apns_binary(data, tokens[0], 0)).once
147
+ expect(connection).to receive(:write).with(apns_binary(data, tokens[1], 1)).once
148
+ expect(connection2).to_not receive(:write)
149
+ pusher.push [notification]
150
+ end
151
+
152
+ it 'stores the error' do
153
+ expect do
154
+ pusher.push [notification]
155
+ end.to change { notification.results }.from(nil).to [NO_ERROR_STATUS_CODE, PROCESSING_ERROR_STATUS_CODE]
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ context 'several notifications' do
163
+ let(:tokens) { (0...10).map { generate:apns_token } }
164
+ let(:notifications) { tokens.map { |token| build :apns_notification, data: data, tokens: [token] } }
165
+
166
+ describe 'success' do
167
+
168
+ before { allow(IO).to receive(:select).and_return [] }
169
+
170
+ it 'writes the notifications to the socket' do
171
+ notifications.each_with_index do |notification, i|
172
+ expect(connection).to receive(:write).with(apns_binary(data, tokens[i], i)).once
173
+ end
174
+ pusher.push notifications
175
+ end
176
+
177
+ it 'flushes the socket contents' do
178
+ expect(connection).to receive(:flush).once
179
+ pusher.push notifications
180
+ end
181
+
182
+ it 'saves results' do
183
+ expect do
184
+ pusher.push notifications
185
+ end.to change { notifications.map { |n| n.results } }.from([nil]*10).to([[NO_ERROR_STATUS_CODE]]*10)
186
+ end
187
+ end
188
+
189
+ describe 'failure' do
190
+
191
+ context 'several async failures' do
192
+
193
+ let(:connection2) { instance_double(APNSConnection).as_null_object }
194
+ let(:connection3) { instance_double(APNSConnection).as_null_object }
195
+ let(:connection4) { instance_double(APNSConnection).as_null_object }
196
+
197
+ before do
198
+ allow(IO).to receive(:select).and_return [], [], [[connection]], [], [], [[connection2]], [], [], [], [], [], [], [[connection3]]
199
+ allow(connection).to receive(:read).with(6).and_return [8, PROCESSING_ERROR_STATUS_CODE, 0].pack('ccN')
200
+ allow(connection2).to receive(:read).with(6).and_return [8, MISSING_DEVICE_TOKEN_STATUS_CODE, 2].pack('ccN')
201
+ allow(connection3).to receive(:read).with(6).and_return [8, INVALID_TOPIC_SIZE_STATUS_CODE, 9].pack('ccN')
202
+ allow(APNSConnection).to receive(:open).with(certificate, sandbox).and_return connection, connection2, connection3, connection4
203
+ end
204
+
205
+ it 'repones the connection' do
206
+ (0..2).each do |i|
207
+ expect(connection).to receive(:write).with(apns_binary(data, tokens[i], i)).once
208
+ end
209
+ expect(connection).to_not receive(:write).with apns_binary(data, tokens[3], 3)
210
+
211
+ expect(connection2).to_not receive(:write).with apns_binary(data, tokens[0], 0)
212
+ (1..3).each do |i|
213
+ expect(connection2).to receive(:write).with(apns_binary(data, tokens[i], i)).once
214
+ end
215
+ expect(connection2).to_not receive(:write).with apns_binary(data, tokens[4], 4)
216
+
217
+ expect(connection3).to_not receive(:write).with apns_binary(data, tokens[2], 2)
218
+ (3..9).each do |i|
219
+ expect(connection3).to receive(:write).with(apns_binary(data, tokens[i], i)).once
220
+ end
221
+
222
+ expect(connection4).to_not receive :write
223
+ pusher.push notifications
224
+ end
225
+
226
+ it 'saves the statuses' do
227
+ expect do
228
+ pusher.push notifications
229
+ end.to change { notifications.map { |n| n.results } }.from([nil]*10).to [
230
+ [PROCESSING_ERROR_STATUS_CODE],
231
+ [NO_ERROR_STATUS_CODE],
232
+ [MISSING_DEVICE_TOKEN_STATUS_CODE],
233
+ [NO_ERROR_STATUS_CODE],
234
+ [NO_ERROR_STATUS_CODE],
235
+ [NO_ERROR_STATUS_CODE],
236
+ [NO_ERROR_STATUS_CODE],
237
+ [NO_ERROR_STATUS_CODE],
238
+ [NO_ERROR_STATUS_CODE],
239
+ [INVALID_TOPIC_SIZE_STATUS_CODE]
240
+ ]
241
+ end
242
+ end
243
+
244
+ context 'several sync failures' do
245
+
246
+ let(:connection2) { instance_double(APNSConnection).as_null_object }
247
+ let(:connection3) { instance_double(APNSConnection).as_null_object }
248
+ let(:connection4) { instance_double(APNSConnection).as_null_object }
249
+
250
+ before do
251
+ allow(IO).to receive(:select).and_return [[connection]], [], [[connection2]], [], [], [], [], [], [], [[connection3]]
252
+ allow(connection).to receive(:read).with(6).and_return [8, PROCESSING_ERROR_STATUS_CODE, 0].pack('ccN')
253
+ allow(connection2).to receive(:read).with(6).and_return [8, MISSING_DEVICE_TOKEN_STATUS_CODE, 2].pack('ccN')
254
+ allow(connection3).to receive(:read).with(6).and_return [8, INVALID_TOPIC_SIZE_STATUS_CODE, 9].pack('ccN')
255
+ allow(APNSConnection).to receive(:open).with(certificate, sandbox).and_return connection, connection2, connection3, connection4
256
+ end
257
+
258
+ it 'repones the connection' do
259
+ expect(connection).to receive(:write).with(apns_binary(data, tokens[0], 0)).once
260
+ expect(connection).to_not receive(:write).with apns_binary(data, tokens[1], 1)
261
+
262
+ expect(connection2).to_not receive(:write).with apns_binary(data, tokens[0], 0)
263
+ (1..2).each do |i|
264
+ expect(connection2).to receive(:write).with(apns_binary(data, tokens[i], i)).once
265
+ end
266
+ expect(connection2).to_not receive(:write).with apns_binary(data, tokens[3], 3)
267
+
268
+ expect(connection3).to_not receive(:write).with apns_binary(data, tokens[2], 2)
269
+ (3..9).each do |i|
270
+ expect(connection3).to receive(:write).with(apns_binary(data, tokens[i], i)).once
271
+ end
272
+
273
+ expect(connection4).to_not receive :write
274
+ pusher.push notifications
275
+ end
276
+
277
+ it 'saves the statuses' do
278
+ expect do
279
+ pusher.push notifications
280
+ end.to change { notifications.map { |n| n.results } }.from([nil]*10).to [
281
+ [PROCESSING_ERROR_STATUS_CODE],
282
+ [NO_ERROR_STATUS_CODE],
283
+ [MISSING_DEVICE_TOKEN_STATUS_CODE],
284
+ [NO_ERROR_STATUS_CODE],
285
+ [NO_ERROR_STATUS_CODE],
286
+ [NO_ERROR_STATUS_CODE],
287
+ [NO_ERROR_STATUS_CODE],
288
+ [NO_ERROR_STATUS_CODE],
289
+ [NO_ERROR_STATUS_CODE],
290
+ [INVALID_TOPIC_SIZE_STATUS_CODE]
291
+ ]
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end