grocer 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 +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
|
+
[](http://travis-ci.org/highgroove/grocer)
|
4
|
+
[](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
|