grocer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +124 -0
- data/Rakefile +6 -0
- data/grocer.gemspec +23 -0
- data/lib/grocer.rb +24 -0
- data/lib/grocer/connection.rb +48 -0
- data/lib/grocer/failed_delivery_attempt.rb +18 -0
- data/lib/grocer/feedback.rb +17 -0
- data/lib/grocer/feedback_connection.rb +29 -0
- data/lib/grocer/invalid_format_error.rb +4 -0
- data/lib/grocer/no_certificate_error.rb +4 -0
- data/lib/grocer/no_gateway_error.rb +4 -0
- data/lib/grocer/no_payload_error.rb +4 -0
- data/lib/grocer/no_port_error.rb +4 -0
- data/lib/grocer/notification.rb +54 -0
- data/lib/grocer/push_connection.rb +29 -0
- data/lib/grocer/pusher.rb +11 -0
- data/lib/grocer/ssl_connection.rb +47 -0
- data/lib/grocer/version.rb +3 -0
- data/spec/fixtures/example.cer +14 -0
- data/spec/fixtures/example.key +9 -0
- data/spec/fixtures/example.pem +23 -0
- data/spec/grocer/connection_spec.rb +83 -0
- data/spec/grocer/failed_delivery_attempt_spec.rb +22 -0
- data/spec/grocer/feedback_connection_spec.rb +61 -0
- data/spec/grocer/feedback_spec.rb +38 -0
- data/spec/grocer/notification_spec.rb +79 -0
- data/spec/grocer/push_connection_spec.rb +62 -0
- data/spec/grocer/pusher_spec.rb +17 -0
- data/spec/grocer/ssl_connection_spec.rb +92 -0
- data/spec/grocer_spec.rb +64 -0
- data/spec/spec_helper.rb +11 -0
- metadata +153 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Steven Harman
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# Grocer
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/highgroove/grocer.png)](http://travis-ci.org/highgroove/grocer)
|
4
|
+
[![Dependency Status](https://gemnasium.com/highgroove/grocer.png)](https://gemnasium.com/highgroove/grocer)
|
5
|
+
|
6
|
+
*grocer* interfaces with the [Apple Push Notification
|
7
|
+
Service](http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html)
|
8
|
+
to send push notifications to iOS devices.
|
9
|
+
|
10
|
+
There are other gems out there to do this, but *grocer* plans to be the
|
11
|
+
cleanest, most extensible, and best maintained.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'grocer'
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
### Connecting
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# `certificate` is the only required option; the rest will default to the values
|
27
|
+
# shown here.
|
28
|
+
#
|
29
|
+
# Information on obtaining a `.pem` file for use with `certificate` is shown
|
30
|
+
# later.
|
31
|
+
pusher = Grocer.pusher.new(
|
32
|
+
certificate: "/path/to/cert.pem", # required
|
33
|
+
passphrase: "", # optional
|
34
|
+
gateway: "gateway.push.apple.com", # optional; See note below.
|
35
|
+
port: 2195 # optional
|
36
|
+
)
|
37
|
+
```
|
38
|
+
|
39
|
+
**NOTE**: The `gateway` option defaults to `gateway.push.apple.com`
|
40
|
+
**only** when running in a production environement, as determined by
|
41
|
+
either the `RAILS_ENV` or `RACK_ENV` environment variables. In all other
|
42
|
+
cases, it defaults to the sandbox gateway,
|
43
|
+
`gateway.sandbox.push.apple.com`.
|
44
|
+
|
45
|
+
### Sending Notifications
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# `device_token` and either `alert` or `badge` are required.
|
49
|
+
#
|
50
|
+
# Information on obtaining `device_token` is shown later.
|
51
|
+
notification = Grocer::Notification.new(
|
52
|
+
device_token: "fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2",
|
53
|
+
alert: "Hello from Grocer!",
|
54
|
+
badge: 42,
|
55
|
+
sound: "siren.aiff", # optional
|
56
|
+
expiry: Time.now + 60*60, # optional; 0 is default, meaning the message is not stored
|
57
|
+
identifier: 1234 # optional
|
58
|
+
)
|
59
|
+
|
60
|
+
pusher.push(notification)
|
61
|
+
```
|
62
|
+
|
63
|
+
It is desirable to reuse the same connection to send multiple notifications, as
|
64
|
+
is recommended by Apple.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
pusher = Grocer.pusher(connection_options)
|
68
|
+
notifications.each do |notification|
|
69
|
+
pusher.push(notification)
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
### Feedback
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
# `certificate` is the only required option; the rest will default to the values
|
77
|
+
# shown here.
|
78
|
+
feedback = Grocer.feedback.new(
|
79
|
+
certificate: "/path/to/cert.pem", # required
|
80
|
+
passphrase: "", # optional
|
81
|
+
gateway: "feedback.push.apple.com", # optional; See note below.
|
82
|
+
port: 2196 # optional
|
83
|
+
)
|
84
|
+
|
85
|
+
feedback.each do |attempt|
|
86
|
+
puts "Device #{attempt.device_token} failed at #{attempt.timestamp}
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
**NOTE**: The `gateway` option defaults to `feedback.push.apple.com`
|
91
|
+
**only** when running in a production environement, as determined by
|
92
|
+
either the `RAILS_ENV` or `RACK_ENV` environment variables. In all other
|
93
|
+
cases, it defaults to the sandbox gateway,
|
94
|
+
`feedback.sandbox.push.apple.com`.
|
95
|
+
|
96
|
+
### Device Token
|
97
|
+
|
98
|
+
A device token is obtained from within the iOS app. More details are in Apple's
|
99
|
+
[Registering for Remote
|
100
|
+
Notifications](http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/IPhoneOSClientImp/IPhoneOSClientImp.html#//apple_ref/doc/uid/TP40008194-CH103-SW1)
|
101
|
+
documentation.
|
102
|
+
|
103
|
+
The key code for this purpose is:
|
104
|
+
|
105
|
+
```objective-c
|
106
|
+
- (void)applicationDidFinishLaunching:(UIApplication *)app {
|
107
|
+
// other setup tasks here....
|
108
|
+
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];
|
109
|
+
}
|
110
|
+
|
111
|
+
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {
|
112
|
+
NSLog(@"Got device token: %@", [devToken description]);
|
113
|
+
|
114
|
+
[self sendProviderDeviceToken:[devToken bytes]]; // custom method; e.g., send to a web service and store
|
115
|
+
}
|
116
|
+
|
117
|
+
- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
|
118
|
+
NSLog(@"Error in registration. Error: %@", err);
|
119
|
+
}
|
120
|
+
```
|
121
|
+
|
122
|
+
### Certificate File
|
123
|
+
|
124
|
+
TODO: Describe how to get a .pem
|
data/Rakefile
ADDED
data/grocer.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/grocer/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ['Andy Lindeman', 'Steven Harman', 'Patrick Van Stee']
|
6
|
+
gem.email = ['andy@highgroove.com', 'steven@highgroove.com', 'vanstee@highgroove.com']
|
7
|
+
gem.description = %q{Pushing your Apple notifications since 2012.}
|
8
|
+
gem.summary = %q{Pushing your Apple notifications since 2012.}
|
9
|
+
gem.homepage = 'https://github.com/highgroove/grocer'
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "grocer"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Grocer::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency 'rspec', '~> 2.9.0'
|
19
|
+
gem.add_development_dependency 'pry', '~> 0.9.8'
|
20
|
+
gem.add_development_dependency 'mocha'
|
21
|
+
gem.add_development_dependency 'bourne'
|
22
|
+
gem.add_development_dependency 'rake'
|
23
|
+
end
|
data/lib/grocer.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'grocer/feedback'
|
2
|
+
require_relative 'grocer/notification'
|
3
|
+
require_relative 'grocer/feedback_connection'
|
4
|
+
require_relative 'grocer/push_connection'
|
5
|
+
require_relative 'grocer/pusher'
|
6
|
+
require_relative 'grocer/version'
|
7
|
+
|
8
|
+
module Grocer
|
9
|
+
|
10
|
+
def self.env
|
11
|
+
ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.feedback(options)
|
15
|
+
connection = FeedbackConnection.new(options)
|
16
|
+
Feedback.new(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.pusher(options)
|
20
|
+
connection = PushConnection.new(options)
|
21
|
+
Pusher.new(connection)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'grocer'
|
2
|
+
require 'grocer/no_certificate_error'
|
3
|
+
require 'grocer/no_gateway_error'
|
4
|
+
require 'grocer/no_port_error'
|
5
|
+
require 'grocer/ssl_connection'
|
6
|
+
|
7
|
+
module Grocer
|
8
|
+
class Connection
|
9
|
+
attr_reader :certificate, :passphrase, :gateway, :port
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@certificate = options.fetch(:certificate) { fail NoCertificateError }
|
13
|
+
@gateway = options.fetch(:gateway) { fail NoGatewayError }
|
14
|
+
@port = options.fetch(:port) { fail NoPortError }
|
15
|
+
@passphrase = options.fetch(:passphrase) { nil }
|
16
|
+
end
|
17
|
+
|
18
|
+
def read(size = nil, buf = nil)
|
19
|
+
with_open_connection do
|
20
|
+
ssl.read(size, buf)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(content)
|
25
|
+
with_open_connection do
|
26
|
+
ssl.write(content)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def ssl
|
33
|
+
@ssl_connection ||= build_connection
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_connection
|
37
|
+
Grocer::SSLConnection.new(certificate: certificate,
|
38
|
+
passphrase: passphrase,
|
39
|
+
gateway: gateway,
|
40
|
+
port: port)
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_open_connection(&block)
|
44
|
+
ssl.connect unless ssl.connected?
|
45
|
+
block.call
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'invalid_format_error'
|
2
|
+
|
3
|
+
module Grocer
|
4
|
+
class FailedDeliveryAttempt
|
5
|
+
LENGTH = 38
|
6
|
+
|
7
|
+
attr_accessor :timestamp, :device_token
|
8
|
+
|
9
|
+
def initialize(binary_tuple)
|
10
|
+
# N => 4 byte timestamp
|
11
|
+
# n => 2 byte token_length
|
12
|
+
# H64 => 32 byte device_token
|
13
|
+
seconds, _, @device_token = binary_tuple.unpack('NnH64')
|
14
|
+
raise InvalidFormatError unless seconds && @device_token
|
15
|
+
@timestamp = Time.at(seconds)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'failed_delivery_attempt'
|
2
|
+
|
3
|
+
module Grocer
|
4
|
+
class Feedback
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(connection)
|
8
|
+
@connection = connection
|
9
|
+
end
|
10
|
+
|
11
|
+
def each
|
12
|
+
while buf = @connection.read(FailedDeliveryAttempt::LENGTH)
|
13
|
+
yield FailedDeliveryAttempt.new(buf)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require_relative 'connection'
|
3
|
+
|
4
|
+
module Grocer
|
5
|
+
class FeedbackConnection < SimpleDelegator
|
6
|
+
|
7
|
+
PRODUCTION_GATEWAY = 'feedback.push.apple.com'
|
8
|
+
SANDBOX_GATEWAY = 'feedback.sandbox.push.apple.com'
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
options = defaults.merge(options)
|
12
|
+
super(Connection.new(options))
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def defaults
|
18
|
+
{
|
19
|
+
gateway: find_default_gateway,
|
20
|
+
port: 2196
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_default_gateway
|
25
|
+
Grocer.env == 'production' ? PRODUCTION_GATEWAY : SANDBOX_GATEWAY
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'no_payload_error'
|
3
|
+
|
4
|
+
module Grocer
|
5
|
+
class Notification
|
6
|
+
attr_accessor :identifier, :expiry, :device_token, :alert, :badge, :sound
|
7
|
+
|
8
|
+
def initialize(payload = {})
|
9
|
+
@identifier = 0
|
10
|
+
|
11
|
+
payload.each do |key, val|
|
12
|
+
send("#{key}=", val)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_bytes
|
17
|
+
validate_payload
|
18
|
+
payload = encoded_payload
|
19
|
+
|
20
|
+
[1, identifier, expiry_epoch_time, device_token_length, sanitized_device_token, payload.length].pack('CNNnH64n') << payload
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_payload
|
26
|
+
fail NoPayloadError unless alert || badge
|
27
|
+
end
|
28
|
+
|
29
|
+
def encoded_payload
|
30
|
+
JSON.dump(payload_hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
def payload_hash
|
34
|
+
aps_hash = { }
|
35
|
+
aps_hash[:alert] = alert if alert
|
36
|
+
aps_hash[:badge] = badge if badge
|
37
|
+
aps_hash[:sound] = sound if sound
|
38
|
+
|
39
|
+
{ aps: aps_hash }
|
40
|
+
end
|
41
|
+
|
42
|
+
def expiry_epoch_time
|
43
|
+
expiry.to_i
|
44
|
+
end
|
45
|
+
|
46
|
+
def sanitized_device_token
|
47
|
+
device_token.tr(' ', '') if device_token
|
48
|
+
end
|
49
|
+
|
50
|
+
def device_token_length
|
51
|
+
32
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require_relative 'connection'
|
3
|
+
|
4
|
+
module Grocer
|
5
|
+
class PushConnection < SimpleDelegator
|
6
|
+
|
7
|
+
PRODUCTION_GATEWAY = 'gateway.push.apple.com'
|
8
|
+
SANDBOX_GATEWAY = 'gateway.sandbox.push.apple.com'
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
options = defaults.merge(options)
|
12
|
+
super(Connection.new(options))
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def defaults
|
18
|
+
{
|
19
|
+
gateway: find_default_gateway,
|
20
|
+
port: 2195
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_default_gateway
|
25
|
+
Grocer.env == 'production' ? PRODUCTION_GATEWAY : SANDBOX_GATEWAY
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'openssl'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Grocer
|
6
|
+
class SSLConnection
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@ssl, :write, :read
|
9
|
+
|
10
|
+
attr_accessor :certificate, :passphrase, :gateway, :port
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
options.each do |key, val|
|
14
|
+
send("#{key}=", val)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def connected?
|
19
|
+
!@ssl.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def connect
|
23
|
+
cert_data = File.read(certificate)
|
24
|
+
context = OpenSSL::SSL::SSLContext.new
|
25
|
+
context.key = OpenSSL::PKey::RSA.new(cert_data, passphrase)
|
26
|
+
context.cert = OpenSSL::X509::Certificate.new(cert_data)
|
27
|
+
|
28
|
+
@sock = TCPSocket.new(gateway, port)
|
29
|
+
@ssl = OpenSSL::SSL::SSLSocket.new(@sock, context)
|
30
|
+
@ssl.sync = true
|
31
|
+
@ssl.connect
|
32
|
+
end
|
33
|
+
|
34
|
+
def disconnect
|
35
|
+
@ssl.close if @ssl
|
36
|
+
@ssl = nil
|
37
|
+
|
38
|
+
@sock.close if @sock
|
39
|
+
@sock = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def reconnect
|
43
|
+
disconnect
|
44
|
+
connect
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIICKzCCAdWgAwIBAgIJAKDDs5zNJQVeMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
3
|
+
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
4
|
+
aWRnaXRzIFB0eSBMdGQwHhcNMTIwMzI5MTg1MDEwWhcNMTIwNDI4MTg1MDEwWjBF
|
5
|
+
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
6
|
+
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANlO
|
7
|
+
UQLaP+NWbniFq2I0yW7AOyOjrKuEBj9i4cVOR1EWN2wnf64gmRucz+DKmxfYr5kq
|
8
|
+
+XT6GIpEdw0wrBGsKlUCAwEAAaOBpzCBpDAdBgNVHQ4EFgQUVkHor75NOFIKInJN
|
9
|
+
VUpmqRbWfsgwdQYDVR0jBG4wbIAUVkHor75NOFIKInJNVUpmqRbWfsihSaRHMEUx
|
10
|
+
CzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRl
|
11
|
+
cm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCgw7OczSUFXjAMBgNVHRMEBTADAQH/MA0G
|
12
|
+
CSqGSIb3DQEBBQUAA0EAzPoxFj8j1uUIEGGsViUXkAH9/uoZuCBy9PtHEJcjSkya
|
13
|
+
TlKHfzEaEsA8pfmHax3gMgYcIbbWuYVR/HTUEnog9Q==
|
14
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,9 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
MIIBOwIBAAJBANlOUQLaP+NWbniFq2I0yW7AOyOjrKuEBj9i4cVOR1EWN2wnf64g
|
3
|
+
mRucz+DKmxfYr5kq+XT6GIpEdw0wrBGsKlUCAwEAAQJBAISneW67Wrrl/WQXfWri
|
4
|
+
/IBeWvvDo9nEK6gwLdCrm3+UZ18Tr9scjdvWsRbHy6fiIYIIZY0KA0mIx1TlPxbD
|
5
|
+
ANUCIQDuc5KeAJZ5bVDWK/Qca6/kAtx6jKH0Q8A4M4vhEynHwwIhAOlMXJqUyt4p
|
6
|
+
As+jG3jRz820QQW5CkjQ4rNCvDgnEjwHAiArzn+5F1KNrE+ViS2nqwD9Wqk2um9m
|
7
|
+
eKvvp0ijaOncEQIgG7dwwQSwXVht9xEfsGjs0Tl7CB0FtcTrSfTBu8IYjn0CIQCh
|
8
|
+
tJEYZA1szHnTdHA2V8FvQ9ZDC3KW6pBvKDQJkqPIaw==
|
9
|
+
-----END RSA PRIVATE KEY-----
|
@@ -0,0 +1,23 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
MIIBOwIBAAJBANlOUQLaP+NWbniFq2I0yW7AOyOjrKuEBj9i4cVOR1EWN2wnf64g
|
3
|
+
mRucz+DKmxfYr5kq+XT6GIpEdw0wrBGsKlUCAwEAAQJBAISneW67Wrrl/WQXfWri
|
4
|
+
/IBeWvvDo9nEK6gwLdCrm3+UZ18Tr9scjdvWsRbHy6fiIYIIZY0KA0mIx1TlPxbD
|
5
|
+
ANUCIQDuc5KeAJZ5bVDWK/Qca6/kAtx6jKH0Q8A4M4vhEynHwwIhAOlMXJqUyt4p
|
6
|
+
As+jG3jRz820QQW5CkjQ4rNCvDgnEjwHAiArzn+5F1KNrE+ViS2nqwD9Wqk2um9m
|
7
|
+
eKvvp0ijaOncEQIgG7dwwQSwXVht9xEfsGjs0Tl7CB0FtcTrSfTBu8IYjn0CIQCh
|
8
|
+
tJEYZA1szHnTdHA2V8FvQ9ZDC3KW6pBvKDQJkqPIaw==
|
9
|
+
-----END RSA PRIVATE KEY-----
|
10
|
+
-----BEGIN CERTIFICATE-----
|
11
|
+
MIICKzCCAdWgAwIBAgIJAKDDs5zNJQVeMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
12
|
+
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
13
|
+
aWRnaXRzIFB0eSBMdGQwHhcNMTIwMzI5MTg1MDEwWhcNMTIwNDI4MTg1MDEwWjBF
|
14
|
+
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
15
|
+
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANlO
|
16
|
+
UQLaP+NWbniFq2I0yW7AOyOjrKuEBj9i4cVOR1EWN2wnf64gmRucz+DKmxfYr5kq
|
17
|
+
+XT6GIpEdw0wrBGsKlUCAwEAAaOBpzCBpDAdBgNVHQ4EFgQUVkHor75NOFIKInJN
|
18
|
+
VUpmqRbWfsgwdQYDVR0jBG4wbIAUVkHor75NOFIKInJNVUpmqRbWfsihSaRHMEUx
|
19
|
+
CzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRl
|
20
|
+
cm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCgw7OczSUFXjAMBgNVHRMEBTADAQH/MA0G
|
21
|
+
CSqGSIb3DQEBBQUAA0EAzPoxFj8j1uUIEGGsViUXkAH9/uoZuCBy9PtHEJcjSkya
|
22
|
+
TlKHfzEaEsA8pfmHax3gMgYcIbbWuYVR/HTUEnog9Q==
|
23
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grocer/connection'
|
3
|
+
|
4
|
+
describe Grocer::Connection do
|
5
|
+
subject { described_class.new(connection_options) }
|
6
|
+
let(:connection_options) { { certificate: '/path/to/cert.pem',
|
7
|
+
gateway: 'push.example.com',
|
8
|
+
port: 443 } }
|
9
|
+
let(:ssl) { stub_everything('SSLConnection') }
|
10
|
+
before do
|
11
|
+
Grocer::SSLConnection.stubs(:new).returns(ssl)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'requires a certificate' do
|
15
|
+
connection_options.delete(:certificate)
|
16
|
+
-> { described_class.new(connection_options) }.should raise_error(Grocer::NoCertificateError)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'can be initialized with a certificate' do
|
20
|
+
subject.certificate.should == '/path/to/cert.pem'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'defaults to an empty passphrase' do
|
24
|
+
subject.passphrase.should be_nil
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'can be initialized with a passphrase' do
|
28
|
+
connection_options[:passphrase] = 'new england clam chowder'
|
29
|
+
subject.passphrase.should == 'new england clam chowder'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'requires a gateway' do
|
33
|
+
connection_options.delete(:gateway)
|
34
|
+
-> { described_class.new(connection_options) }.should raise_error(Grocer::NoGatewayError)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'can be initialized with a gateway' do
|
38
|
+
subject.gateway.should == 'push.example.com'
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'requires a port' do
|
42
|
+
connection_options.delete(:port)
|
43
|
+
-> { described_class.new(connection_options) }.should raise_error(Grocer::NoPortError)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'can be initialized with a port' do
|
47
|
+
subject.port.should == 443
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'an open SSLConnection' do
|
51
|
+
before do
|
52
|
+
ssl.stubs(:connected?).returns(true)
|
53
|
+
end
|
54
|
+
|
55
|
+
it '#write delegates to open SSLConnection' do
|
56
|
+
subject.write('Apples to Oranges')
|
57
|
+
ssl.should have_received(:write).with('Apples to Oranges')
|
58
|
+
end
|
59
|
+
|
60
|
+
it '#read delegates to open SSLConnection' do
|
61
|
+
subject.read(42, 'IO')
|
62
|
+
ssl.should have_received(:read).with(42, 'IO')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'a closed SSLConnection' do
|
67
|
+
before do
|
68
|
+
ssl.stubs(:connected?).returns(false)
|
69
|
+
end
|
70
|
+
|
71
|
+
it '#write connects SSLConnection and delegates to it' do
|
72
|
+
subject.write('Apples to Oranges')
|
73
|
+
ssl.should have_received(:connect)
|
74
|
+
ssl.should have_received(:write).with('Apples to Oranges')
|
75
|
+
end
|
76
|
+
|
77
|
+
it '#read connects SSLConnection delegates to open SSLConnection' do
|
78
|
+
subject.read(42, 'IO')
|
79
|
+
ssl.should have_received(:connect)
|
80
|
+
ssl.should have_received(:read).with(42, 'IO')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grocer/failed_delivery_attempt'
|
3
|
+
|
4
|
+
describe Grocer::FailedDeliveryAttempt do
|
5
|
+
let(:timestamp) { Time.utc(1995, 12, 21) }
|
6
|
+
let(:device_token) { 'fe15a27d5df3c34778defb1f4f3980265cc52c0c047682223be59fb68500a9a2' }
|
7
|
+
let(:binary_tuple) { [timestamp.to_i, 32, device_token].pack('NnH64') }
|
8
|
+
let(:invalid_binary_tuple) { 'totally not the right format' }
|
9
|
+
|
10
|
+
describe 'decoding' do
|
11
|
+
it 'accepts a binary tuple and sets each attribute' do
|
12
|
+
failed_delivery_attempt = described_class.new(binary_tuple)
|
13
|
+
failed_delivery_attempt.timestamp.should == timestamp
|
14
|
+
failed_delivery_attempt.device_token.should == device_token
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'raises an exception when there are problems decoding' do
|
18
|
+
-> { described_class.new(invalid_binary_tuple) }.should
|
19
|
+
raise_error(Grocer::InvalidFormatError)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grocer/feedback_connection'
|
3
|
+
|
4
|
+
describe Grocer::FeedbackConnection do
|
5
|
+
subject { described_class.new(options) }
|
6
|
+
let(:options) { { certificate: '/path/to/cert.pem' } }
|
7
|
+
let(:connection) { stub("Connection") }
|
8
|
+
|
9
|
+
it 'delegates reading to the Connection' do
|
10
|
+
Grocer::Connection.any_instance.expects(:read).with(42, 'lolIO')
|
11
|
+
subject.read(42, 'lolIO')
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'delegates writing to the Connection' do
|
15
|
+
Grocer::Connection.any_instance.expects(:write).with('Note Eye Fly')
|
16
|
+
subject.write('Note Eye Fly')
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'can be initialized with a certificate' do
|
20
|
+
subject.certificate.should == '/path/to/cert.pem'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'can be initialized with a passphrase' do
|
24
|
+
options[:passphrase] = 'open sesame'
|
25
|
+
subject.passphrase.should == 'open sesame'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'defaults to Apple feedback gateway in production environment' do
|
29
|
+
Grocer.stubs(:env).returns('production')
|
30
|
+
subject.gateway.should == 'feedback.push.apple.com'
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'defaults to the sandboxed Apple feedback gateway in development environment' do
|
34
|
+
Grocer.stubs(:env).returns('development')
|
35
|
+
subject.gateway.should == 'feedback.sandbox.push.apple.com'
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'defaults to the sandboxed Apple feedback gateway in test environment' do
|
39
|
+
Grocer.stubs(:env).returns('test')
|
40
|
+
subject.gateway.should == 'feedback.sandbox.push.apple.com'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'defaults to the sandboxed Apple feedback gateway for other random values' do
|
44
|
+
Grocer.stubs(:env).returns('random')
|
45
|
+
subject.gateway.should == 'feedback.sandbox.push.apple.com'
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'can be initialized with a gateway' do
|
49
|
+
options[:gateway] = 'gateway.example.com'
|
50
|
+
subject.gateway.should == 'gateway.example.com'
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'defaults to 2196 as the port' do
|
54
|
+
subject.port.should == 2196
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'can be initialized with a port' do
|
58
|
+
options[:port] = 443
|
59
|
+
subject.port.should == 443
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grocer/feedback'
|
3
|
+
|
4
|
+
describe Grocer::Feedback do
|
5
|
+
def stub_feedback
|
6
|
+
# "Reads" two failed deliveries: one on Jan 1; the other on Jan 2
|
7
|
+
connection.stubs(:read).
|
8
|
+
with(38).
|
9
|
+
returns([jan1.to_i, 32, device_token].pack('NnH64')).
|
10
|
+
then.
|
11
|
+
returns([jan2.to_i, 32, device_token].pack('NnH64')).
|
12
|
+
then.
|
13
|
+
returns(nil)
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:connection) { stub_everything }
|
17
|
+
let(:jan1) { Time.utc(2012, 1, 1) }
|
18
|
+
let(:jan2) { Time.utc(2012, 1, 2) }
|
19
|
+
let(:device_token) { 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2' }
|
20
|
+
|
21
|
+
subject { described_class.new(connection) }
|
22
|
+
|
23
|
+
it 'is enumerable' do
|
24
|
+
subject.should be_kind_of(Enumerable)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'reads failed delivery attempt messages from the connection' do
|
28
|
+
stub_feedback
|
29
|
+
|
30
|
+
delivery_attempts = subject.to_a
|
31
|
+
|
32
|
+
delivery_attempts[0].timestamp.should == jan1
|
33
|
+
delivery_attempts[0].device_token.should == device_token
|
34
|
+
|
35
|
+
delivery_attempts[1].timestamp.should == jan2
|
36
|
+
delivery_attempts[1].device_token.should == device_token
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grocer/notification'
|
3
|
+
|
4
|
+
describe Grocer::Notification do
|
5
|
+
describe 'binary format' do
|
6
|
+
let(:notification) { described_class.new(payload_options) }
|
7
|
+
let(:payload_from_bytes) { notification.to_bytes[45..-1] }
|
8
|
+
let(:payload_dictionary_from_bytes) { JSON.parse(payload_from_bytes, symbolize_names: true) }
|
9
|
+
let(:payload_options) { { alert: 'hi', badge: 2, sound: 'siren.aiff' } }
|
10
|
+
|
11
|
+
subject { notification.to_bytes }
|
12
|
+
|
13
|
+
it 'sets the command byte to 1' do
|
14
|
+
subject[0].should == "\x01"
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'defaults the identifer to 0' do
|
18
|
+
subject[1...5].should == "\x00\x00\x00\x00"
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'allows the identifier to be set' do
|
22
|
+
notification.identifier = 1234
|
23
|
+
subject[1...5].should == [1234].pack('N')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'defaults expiry to zero' do
|
27
|
+
subject[5...9].should == "\x00\x00\x00\x00"
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'allows the expiry to be set' do
|
31
|
+
expiry = notification.expiry = Time.utc(2013, 3, 24, 12, 34, 56)
|
32
|
+
subject[5...9].should == [expiry.to_i].pack('N')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'encodes the device token length as 32' do
|
36
|
+
subject[9...11].should == "\x00\x20"
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'encodes the device token as a 256-bit integer' do
|
40
|
+
token = notification.device_token = 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'
|
41
|
+
subject[11...43].should == ['fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'].pack('H*')
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'as a convenience, flattens the device token to remove spaces' do
|
45
|
+
token = notification.device_token = 'fe15 a27d 5df3c3 4778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'
|
46
|
+
subject[11...43].should == ['fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'].pack('H*')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'encodes the payload length' do
|
50
|
+
notification.alert = 'Hello World!'
|
51
|
+
subject[43...45].should == [payload_from_bytes.length].pack('n')
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'encodes alert as part of the payload' do
|
55
|
+
notification.alert = 'Hello World!'
|
56
|
+
payload_dictionary_from_bytes[:aps][:alert].should == 'Hello World!'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'encodes badge as part of the payload' do
|
60
|
+
notification.badge = 42
|
61
|
+
payload_dictionary_from_bytes[:aps][:badge].should == 42
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'encodes sound as part of the payload' do
|
65
|
+
notification.sound = 'siren.aiff'
|
66
|
+
payload_dictionary_from_bytes[:aps][:sound].should == 'siren.aiff'
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'encodes custom payload attributes'
|
70
|
+
|
71
|
+
context 'invalid payload' do
|
72
|
+
let(:payload_options) { Hash.new }
|
73
|
+
|
74
|
+
it 'raises an error when neither alert nor badge is specified' do
|
75
|
+
-> { notification.to_bytes }.should raise_error(Grocer::NoPayloadError)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grocer/push_connection'
|
3
|
+
|
4
|
+
describe Grocer::PushConnection do
|
5
|
+
subject { described_class.new(options) }
|
6
|
+
let(:options) { { certificate: '/path/to/cert.pem' } }
|
7
|
+
let(:connection) { stub("Connection") }
|
8
|
+
|
9
|
+
it 'delegates reading to the Connection' do
|
10
|
+
Grocer::Connection.any_instance.expects(:read).with(42, 'lolIO')
|
11
|
+
subject.read(42, 'lolIO')
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'delegates writing to the Connection' do
|
15
|
+
Grocer::Connection.any_instance.expects(:write).with('Note Eye Fly')
|
16
|
+
subject.write('Note Eye Fly')
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'can be initialized with a certificate' do
|
20
|
+
subject.certificate.should == '/path/to/cert.pem'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'can be initialized with a passphrase' do
|
24
|
+
options[:passphrase] = 'open sesame'
|
25
|
+
subject.passphrase.should == 'open sesame'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'defaults to Apple push gateway in production environment' do
|
29
|
+
Grocer.stubs(:env).returns('production')
|
30
|
+
subject.gateway.should == 'gateway.push.apple.com'
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'defaults to the sandboxed Apple push gateway in development environment' do
|
34
|
+
Grocer.stubs(:env).returns('development')
|
35
|
+
subject.gateway.should == 'gateway.sandbox.push.apple.com'
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'defaults to the sandboxed Apple push gateway in test environment' do
|
39
|
+
Grocer.stubs(:env).returns('test')
|
40
|
+
subject.gateway.should == 'gateway.sandbox.push.apple.com'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'defaults to the sandboxed Apple push gateway for other random values' do
|
44
|
+
Grocer.stubs(:env).returns('random')
|
45
|
+
subject.gateway.should == 'gateway.sandbox.push.apple.com'
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'can be initialized with a gateway' do
|
49
|
+
options[:gateway] = 'gateway.example.com'
|
50
|
+
subject.gateway.should == 'gateway.example.com'
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'defaults to 2195 as the port' do
|
54
|
+
subject.port.should == 2195
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'can be initialized with a port' do
|
58
|
+
options[:port] = 443
|
59
|
+
subject.port.should == 443
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grocer/pusher'
|
3
|
+
|
4
|
+
describe Grocer::Pusher do
|
5
|
+
let(:connection) { stub_everything }
|
6
|
+
|
7
|
+
subject { described_class.new(connection) }
|
8
|
+
|
9
|
+
describe '#push' do
|
10
|
+
it 'serializes a notification and sends it via the connection' do
|
11
|
+
notification = stub(:to_bytes => 'abc123')
|
12
|
+
subject.push(notification)
|
13
|
+
|
14
|
+
connection.should have_received(:write).with('abc123')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grocer/ssl_connection'
|
3
|
+
|
4
|
+
describe Grocer::SSLConnection do
|
5
|
+
def stub_sockets
|
6
|
+
TCPSocket.stubs(:new).returns(mock_socket)
|
7
|
+
OpenSSL::SSL::SSLSocket.stubs(:new).returns(mock_ssl)
|
8
|
+
end
|
9
|
+
|
10
|
+
def stub_certificate
|
11
|
+
example_data = File.read(File.dirname(__FILE__) + '/../fixtures/example.pem')
|
12
|
+
File.stubs(:read).with(connection_options[:certificate]).returns(example_data)
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:mock_socket) { stub_everything }
|
16
|
+
let(:mock_ssl) { stub_everything }
|
17
|
+
|
18
|
+
let(:connection_options) {
|
19
|
+
{
|
20
|
+
certificate: '/path/to/cert.pem',
|
21
|
+
passphrase: 'abc123',
|
22
|
+
gateway: 'gateway.push.highgroove.com',
|
23
|
+
port: 1234
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
subject { described_class.new(connection_options) }
|
28
|
+
|
29
|
+
describe 'configuration' do
|
30
|
+
it 'is initialized with a certificate' do
|
31
|
+
subject.certificate.should == connection_options[:certificate]
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'is initialized with a passphrase' do
|
35
|
+
subject.passphrase.should == connection_options[:passphrase]
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'is initialized with a gateway' do
|
39
|
+
subject.gateway.should == connection_options[:gateway]
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'is initialized with a port' do
|
43
|
+
subject.port.should == connection_options[:port]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'connecting' do
|
48
|
+
before do
|
49
|
+
stub_sockets
|
50
|
+
stub_certificate
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'sets up an socket connection' do
|
54
|
+
subject.connect
|
55
|
+
TCPSocket.should have_received(:new).with(connection_options[:gateway],
|
56
|
+
connection_options[:port])
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'sets up an SSL connection' do
|
60
|
+
subject.connect
|
61
|
+
OpenSSL::SSL::SSLSocket.should have_received(:new).with(mock_socket, anything)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'writing data' do
|
66
|
+
before do
|
67
|
+
stub_sockets
|
68
|
+
stub_certificate
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'writes data to the SSL connection' do
|
72
|
+
subject.connect
|
73
|
+
subject.write('abc123')
|
74
|
+
|
75
|
+
mock_ssl.should have_received(:write).with('abc123')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'reading data' do
|
80
|
+
before do
|
81
|
+
stub_sockets
|
82
|
+
stub_certificate
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'reads data from the SSL connection' do
|
86
|
+
subject.connect
|
87
|
+
subject.read(42)
|
88
|
+
|
89
|
+
mock_ssl.should have_received(:read).with(42)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/spec/grocer_spec.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grocer'
|
3
|
+
|
4
|
+
describe Grocer do
|
5
|
+
subject { described_class }
|
6
|
+
|
7
|
+
describe '.env' do
|
8
|
+
let(:environment) { nil }
|
9
|
+
before do
|
10
|
+
ENV.stubs(:[]).with('RAILS_ENV').returns(environment)
|
11
|
+
ENV.stubs(:[]).with('RACK_ENV').returns(environment)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'defaults to development' do
|
15
|
+
subject.env.should == 'development'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'reads RAILS_ENV from ENV' do
|
19
|
+
ENV.stubs(:[]).with('RAILS_ENV').returns('staging')
|
20
|
+
subject.env.should == 'staging'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'reads RACK_ENV from ENV' do
|
24
|
+
ENV.stubs(:[]).with('RACK_ENV').returns('staging')
|
25
|
+
subject.env.should == 'staging'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'API facade' do
|
30
|
+
let(:connection_options) { stub('connection options') }
|
31
|
+
|
32
|
+
describe '.pusher' do
|
33
|
+
before do
|
34
|
+
Grocer::PushConnection.stubs(:new).returns(stub('PushConnection'))
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'gets a Pusher' do
|
38
|
+
subject.pusher(connection_options).should be_a Grocer::Pusher
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'passes the connection options on to the underlying Connection' do
|
42
|
+
subject.pusher(connection_options)
|
43
|
+
Grocer::PushConnection.should have_received(:new).with(connection_options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '.feedback' do
|
48
|
+
before do
|
49
|
+
Grocer::FeedbackConnection.stubs(:new).returns(stub('FeedbackConnection'))
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'gets Feedback' do
|
53
|
+
subject.feedback(connection_options).should be_a Grocer::Feedback
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'passes the connection options on to the underlying Connection' do
|
57
|
+
subject.feedback(connection_options)
|
58
|
+
Grocer::FeedbackConnection.should have_received(:new).with(connection_options)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
require 'mocha'
|
3
|
+
require 'bourne'
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
7
|
+
config.run_all_when_everything_filtered = true
|
8
|
+
config.filter_run :focus
|
9
|
+
|
10
|
+
config.mock_with :mocha
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: grocer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andy Lindeman
|
9
|
+
- Steven Harman
|
10
|
+
- Patrick Van Stee
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2012-03-31 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rspec
|
18
|
+
requirement: &70193834218040 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.9.0
|
24
|
+
type: :development
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *70193834218040
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: &70193834216820 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.9.8
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: *70193834216820
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: mocha
|
40
|
+
requirement: &70193834216380 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
type: :development
|
47
|
+
prerelease: false
|
48
|
+
version_requirements: *70193834216380
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: bourne
|
51
|
+
requirement: &70193834215860 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
type: :development
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: *70193834215860
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: rake
|
62
|
+
requirement: &70193834215440 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: *70193834215440
|
71
|
+
description: Pushing your Apple notifications since 2012.
|
72
|
+
email:
|
73
|
+
- andy@highgroove.com
|
74
|
+
- steven@highgroove.com
|
75
|
+
- vanstee@highgroove.com
|
76
|
+
executables: []
|
77
|
+
extensions: []
|
78
|
+
extra_rdoc_files: []
|
79
|
+
files:
|
80
|
+
- .gitignore
|
81
|
+
- .rspec
|
82
|
+
- .travis.yml
|
83
|
+
- Gemfile
|
84
|
+
- LICENSE
|
85
|
+
- README.md
|
86
|
+
- Rakefile
|
87
|
+
- grocer.gemspec
|
88
|
+
- lib/grocer.rb
|
89
|
+
- lib/grocer/connection.rb
|
90
|
+
- lib/grocer/failed_delivery_attempt.rb
|
91
|
+
- lib/grocer/feedback.rb
|
92
|
+
- lib/grocer/feedback_connection.rb
|
93
|
+
- lib/grocer/invalid_format_error.rb
|
94
|
+
- lib/grocer/no_certificate_error.rb
|
95
|
+
- lib/grocer/no_gateway_error.rb
|
96
|
+
- lib/grocer/no_payload_error.rb
|
97
|
+
- lib/grocer/no_port_error.rb
|
98
|
+
- lib/grocer/notification.rb
|
99
|
+
- lib/grocer/push_connection.rb
|
100
|
+
- lib/grocer/pusher.rb
|
101
|
+
- lib/grocer/ssl_connection.rb
|
102
|
+
- lib/grocer/version.rb
|
103
|
+
- spec/fixtures/example.cer
|
104
|
+
- spec/fixtures/example.key
|
105
|
+
- spec/fixtures/example.pem
|
106
|
+
- spec/grocer/connection_spec.rb
|
107
|
+
- spec/grocer/failed_delivery_attempt_spec.rb
|
108
|
+
- spec/grocer/feedback_connection_spec.rb
|
109
|
+
- spec/grocer/feedback_spec.rb
|
110
|
+
- spec/grocer/notification_spec.rb
|
111
|
+
- spec/grocer/push_connection_spec.rb
|
112
|
+
- spec/grocer/pusher_spec.rb
|
113
|
+
- spec/grocer/ssl_connection_spec.rb
|
114
|
+
- spec/grocer_spec.rb
|
115
|
+
- spec/spec_helper.rb
|
116
|
+
homepage: https://github.com/highgroove/grocer
|
117
|
+
licenses: []
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
124
|
+
requirements:
|
125
|
+
- - ! '>='
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
requirements: []
|
135
|
+
rubyforge_project:
|
136
|
+
rubygems_version: 1.8.17
|
137
|
+
signing_key:
|
138
|
+
specification_version: 3
|
139
|
+
summary: Pushing your Apple notifications since 2012.
|
140
|
+
test_files:
|
141
|
+
- spec/fixtures/example.cer
|
142
|
+
- spec/fixtures/example.key
|
143
|
+
- spec/fixtures/example.pem
|
144
|
+
- spec/grocer/connection_spec.rb
|
145
|
+
- spec/grocer/failed_delivery_attempt_spec.rb
|
146
|
+
- spec/grocer/feedback_connection_spec.rb
|
147
|
+
- spec/grocer/feedback_spec.rb
|
148
|
+
- spec/grocer/notification_spec.rb
|
149
|
+
- spec/grocer/push_connection_spec.rb
|
150
|
+
- spec/grocer/pusher_spec.rb
|
151
|
+
- spec/grocer/ssl_connection_spec.rb
|
152
|
+
- spec/grocer_spec.rb
|
153
|
+
- spec/spec_helper.rb
|