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.
- data/.gitignore +35 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +55 -0
- data/LICENSE +22 -0
- data/README.md +56 -0
- data/Rakefile +12 -0
- data/examples/apns copy.rb +17 -0
- data/examples/apns.rb +16 -0
- data/examples/gcm copy.rb +16 -0
- data/examples/gcm.rb +16 -0
- data/lib/ruby-push-notifications.rb +8 -0
- data/lib/ruby-push-notifications/apns.rb +19 -0
- data/lib/ruby-push-notifications/apns/apns_connection.rb +45 -0
- data/lib/ruby-push-notifications/apns/apns_notification.rb +62 -0
- data/lib/ruby-push-notifications/apns/apns_pusher.rb +51 -0
- data/lib/ruby-push-notifications/gcm.rb +7 -0
- data/lib/ruby-push-notifications/gcm/gcm_connection.rb +31 -0
- data/lib/ruby-push-notifications/gcm/gcm_error.rb +14 -0
- data/lib/ruby-push-notifications/gcm/gcm_notification.rb +21 -0
- data/lib/ruby-push-notifications/gcm/gcm_pusher.rb +17 -0
- data/lib/ruby-push-notifications/gcm/gcm_response.rb +52 -0
- data/lib/ruby-push-notifications/gcm/gcm_result.rb +38 -0
- data/lib/ruby-push-notifications/version.rb +3 -0
- data/ruby-push-notifications.gemspec +26 -0
- data/spec/factories.rb +10 -0
- data/spec/factories/notifications.rb +15 -0
- data/spec/ruby-push-notifications/apns/apns_connection_spec.rb +79 -0
- data/spec/ruby-push-notifications/apns/apns_notification_spec.rb +30 -0
- data/spec/ruby-push-notifications/apns/apns_pusher_spec.rb +299 -0
- data/spec/ruby-push-notifications/gcm/gcm_connection_spec.rb +32 -0
- data/spec/ruby-push-notifications/gcm/gcm_notification_spec.rb +24 -0
- data/spec/ruby-push-notifications/gcm/gcm_pusher_spec.rb +45 -0
- data/spec/ruby-push-notifications/gcm/gcm_response_spec.rb +82 -0
- data/spec/spec_helper.rb +110 -0
- data/spec/support/dummy.pem +44 -0
- data/spec/support/factory_girl.rb +5 -0
- 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,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
|