ruby-push-notifications 0.0.1

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