grocer 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -2
- data/CHANGELOG.md +17 -7
- data/Gemfile +0 -4
- data/README.md +18 -4
- data/grocer.gemspec +0 -2
- data/lib/grocer/notification.rb +10 -0
- data/lib/grocer/passbook_notification.rb +20 -0
- data/lib/grocer/push_connection.rb +1 -1
- data/lib/grocer/version.rb +1 -1
- data/spec/grocer/connection_spec.rb +8 -8
- data/spec/grocer/extensions/deep_symbolize_keys_spec.rb +5 -5
- data/spec/grocer/failed_delivery_attempt_spec.rb +2 -2
- data/spec/grocer/feedback_connection_spec.rb +9 -9
- data/spec/grocer/feedback_spec.rb +5 -5
- data/spec/grocer/notification_reader_spec.rb +7 -7
- data/spec/grocer/notification_spec.rb +15 -52
- data/spec/grocer/passbook_notification_spec.rb +23 -0
- data/spec/grocer/push_connection_spec.rb +14 -9
- data/spec/grocer/server_spec.rb +1 -1
- data/spec/grocer/shared_examples_for_notifications.rb +41 -0
- data/spec/grocer/ssl_connection_spec.rb +5 -5
- data/spec/grocer/ssl_server_spec.rb +2 -2
- data/spec/grocer_spec.rb +6 -7
- data/spec/spec_helper.rb +1 -1
- metadata +35 -45
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,20 +1,30 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## 0.3.0
|
4
|
+
|
5
|
+
* Add `Grocer::PassbookNotification` for sending, well... Passbook
|
6
|
+
notifications. This kind of notification requires no payload.
|
7
|
+
* Determining current environment is case-insensitive ([Oriol
|
8
|
+
Gual](https://github.com/oriolgual))
|
9
|
+
|
3
10
|
## 0.2.0
|
4
11
|
|
5
|
-
* Don't retry connection when the certificate has expired. (Kyle
|
6
|
-
Jesse
|
12
|
+
* Don't retry connection when the certificate has expired. ([Kyle
|
13
|
+
Drake](https://github.com/kyledrake) and [Jesse
|
14
|
+
Storimer](https://github.com/jstorimer))
|
7
15
|
|
8
16
|
## 0.1.1
|
9
17
|
|
10
|
-
* Warn that `jruby-openssl` is needed on JRuby platform. (Kyle
|
18
|
+
* Warn that `jruby-openssl` is needed on JRuby platform. ([Kyle
|
19
|
+
Drake](https://github.com/kyledrake))
|
11
20
|
|
12
21
|
## 0.1.0
|
13
22
|
|
14
|
-
*
|
15
|
-
*
|
16
|
-
|
17
|
-
* Certificate can be any object that responds to #read (Kyle
|
23
|
+
* Supports non-ASCII characters in notifications
|
24
|
+
* Enables socket keepalive option on APNS client sockets ([Kyle
|
25
|
+
Drake](https://github.com/kyledrake))
|
26
|
+
* Certificate can be any object that responds to #read ([Kyle
|
27
|
+
Drake](https://github.com/kyledrake))
|
18
28
|
|
19
29
|
## 0.0.13
|
20
30
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Grocer
|
2
2
|
|
3
|
-
[![Build Status](https://
|
3
|
+
[![Build Status](https://api.travis-ci.org/highgroove/grocer.png?branch=master)](https://travis-ci.org/highgroove/grocer)
|
4
4
|
[![Dependency Status](https://gemnasium.com/highgroove/grocer.png)](https://gemnasium.com/highgroove/grocer)
|
5
5
|
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/highgroove/grocer)
|
6
6
|
|
@@ -13,7 +13,7 @@ cleanest, most extensible, and friendliest.
|
|
13
13
|
|
14
14
|
## Requirements
|
15
15
|
|
16
|
-
* Ruby/MRI 1.9.x
|
16
|
+
* Ruby/MRI 1.9.x, JRuby 1.7.x in 1.9 mode, Rubinius in 1.9 mode
|
17
17
|
|
18
18
|
## Installation
|
19
19
|
|
@@ -50,7 +50,7 @@ pusher = Grocer.pusher(
|
|
50
50
|
|
51
51
|
#### Notes
|
52
52
|
|
53
|
-
* `certificate`: If you don't have the certificate stored in a file, you
|
53
|
+
* `certificate`: If you don't have the certificate stored in a file, you
|
54
54
|
can pass any object that responds to `read`.
|
55
55
|
Example: `certificate: StringIO.new(pem_string)`
|
56
56
|
* `gateway`: Defaults to different values depending on the `RAILS_ENV` or
|
@@ -109,6 +109,20 @@ notification = Grocer::Notification.new(
|
|
109
109
|
# {"aps": {"alert": "Hello from Grocer"}, "acme2": ["bang", "whiz"]}
|
110
110
|
```
|
111
111
|
|
112
|
+
#### Passbook Notifications
|
113
|
+
|
114
|
+
A `Grocer::PassbookNotification` is a specialized kind of notification which
|
115
|
+
does not require any payload. That is, you need not (and *[Apple explicitly says
|
116
|
+
not to](http://developer.apple.com/library/ios/#Documentation/UserExperience/Conceptual/PassKit_PG/Chapters/Updating.html#//apple_ref/doc/uid/TP40012195-CH5-SW1)*)
|
117
|
+
send any payload for a Passbook notification. If you do, it will be ignored.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
notification = Grocer::PassbookNotification.new(device_token: "...")
|
121
|
+
|
122
|
+
# Generates a JSON payload like:
|
123
|
+
# {"aps": {}}
|
124
|
+
```
|
125
|
+
|
112
126
|
### Feedback
|
113
127
|
|
114
128
|
```ruby
|
@@ -169,7 +183,7 @@ describe "apple push notifications" do
|
|
169
183
|
|
170
184
|
Timeout.timeout(3) {
|
171
185
|
notification = @server.notifications.pop # blocking
|
172
|
-
notification.alert.
|
186
|
+
expect(notification.alert).to eq("An awesome thing happened")
|
173
187
|
}
|
174
188
|
end
|
175
189
|
end
|
data/grocer.gemspec
CHANGED
@@ -26,8 +26,6 @@ Gem::Specification.new do |gem|
|
|
26
26
|
gem.require_paths = ["lib"]
|
27
27
|
gem.version = Grocer::VERSION
|
28
28
|
|
29
|
-
gem.add_dependency 'json'
|
30
|
-
|
31
29
|
gem.add_development_dependency 'rspec', '~> 2.11'
|
32
30
|
gem.add_development_dependency 'pry', '~> 0.9.8'
|
33
31
|
gem.add_development_dependency 'mocha'
|
data/lib/grocer/notification.rb
CHANGED
@@ -2,10 +2,20 @@ require 'json'
|
|
2
2
|
require 'grocer/no_payload_error'
|
3
3
|
|
4
4
|
module Grocer
|
5
|
+
# Public: An object used to send notifications to APNS.
|
5
6
|
class Notification
|
6
7
|
attr_accessor :identifier, :expiry, :device_token, :alert, :badge, :sound,
|
7
8
|
:custom
|
8
9
|
|
10
|
+
# Public: Initialize a new Grocer::Notification. You must specify at least an `alert` or `badge`.
|
11
|
+
#
|
12
|
+
# payload - The Hash of notification parameters and payload to be sent to APNS.:
|
13
|
+
# :device_token - The String representing to device token sent to APNS.
|
14
|
+
# :alert - The String or Hash to be sent as the alert portion of the payload. (optional)
|
15
|
+
# :badge - The Integer to be sent as the badge portion of the payload. (optional)
|
16
|
+
# :sound - The String representing the sound portion of the payload. (optional)
|
17
|
+
# :expiry - The Integer representing UNIX epoch date sent to APNS as the notification expiry. (default: 0)
|
18
|
+
# :identifier - The arbitrary value sent to APNS to uniquely this notification. (default: 0)
|
9
19
|
def initialize(payload = {})
|
10
20
|
@identifier = 0
|
11
21
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'grocer/notification'
|
2
|
+
|
3
|
+
module Grocer
|
4
|
+
# Public: A specialized form of a Grocer::Notification which requires neither
|
5
|
+
# an alert nor badge to be present in the payload. It requires only the
|
6
|
+
# `device_token`, and allows an optional `expiry` and `identifier` to be set.
|
7
|
+
#
|
8
|
+
# Examples
|
9
|
+
#
|
10
|
+
# Grocer::PassbookNotification.new(device_token: '...')
|
11
|
+
class PassbookNotification < Notification
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def validate_payload
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
data/lib/grocer/version.rb
CHANGED
@@ -12,25 +12,25 @@ describe Grocer::Connection do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
it 'can be initialized with a certificate' do
|
15
|
-
subject.certificate.
|
15
|
+
expect(subject.certificate).to eq('/path/to/cert.pem')
|
16
16
|
end
|
17
17
|
|
18
18
|
it 'defaults to an empty passphrase' do
|
19
|
-
subject.passphrase.
|
19
|
+
expect(subject.passphrase).to be_nil
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'can be initialized with a passphrase' do
|
23
23
|
connection_options[:passphrase] = 'new england clam chowder'
|
24
|
-
subject.passphrase.
|
24
|
+
expect(subject.passphrase).to eq('new england clam chowder')
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'defaults to 3 retries' do
|
28
|
-
subject.retries.
|
28
|
+
expect(subject.retries).to eq(3)
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'can be initialized with a number of retries' do
|
32
32
|
connection_options[:retries] = 2
|
33
|
-
subject.retries.
|
33
|
+
expect(subject.retries).to eq(2)
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'requires a gateway' do
|
@@ -39,7 +39,7 @@ describe Grocer::Connection do
|
|
39
39
|
end
|
40
40
|
|
41
41
|
it 'can be initialized with a gateway' do
|
42
|
-
subject.gateway.
|
42
|
+
expect(subject.gateway).to eq('push.example.com')
|
43
43
|
end
|
44
44
|
|
45
45
|
it 'requires a port' do
|
@@ -48,14 +48,14 @@ describe Grocer::Connection do
|
|
48
48
|
end
|
49
49
|
|
50
50
|
it 'can be initialized with a port' do
|
51
|
-
subject.port.
|
51
|
+
expect(subject.port).to eq(443)
|
52
52
|
end
|
53
53
|
|
54
54
|
it 'can open the connection to the apple push notification service' do
|
55
55
|
subject.connect
|
56
56
|
ssl.should have_received(:connect)
|
57
57
|
end
|
58
|
-
|
58
|
+
|
59
59
|
it 'raises CertificateExpiredError for OpenSSL::SSL::SSLError with /certificate expired/i message' do
|
60
60
|
ssl.stubs(:write).raises(OpenSSL::SSL::SSLError.new('certificate expired'))
|
61
61
|
-> {subject.write('abc123')}.should raise_error(Grocer::CertificateExpiredError)
|
@@ -15,22 +15,22 @@ describe Grocer::Extensions::DeepSymbolizeKeys do
|
|
15
15
|
end
|
16
16
|
|
17
17
|
it 'does not change nested symbols' do
|
18
|
-
nested_symbols.deep_symbolize_keys.
|
18
|
+
expect(nested_symbols.deep_symbolize_keys).to eq(nested_symbols)
|
19
19
|
end
|
20
20
|
|
21
21
|
it 'symbolizes nested strings' do
|
22
|
-
nested_strings.deep_symbolize_keys.
|
22
|
+
expect(nested_strings.deep_symbolize_keys).to eq(nested_symbols)
|
23
23
|
end
|
24
24
|
|
25
25
|
it 'symbolizes a mix of nested strings and symbols' do
|
26
|
-
nested_mixed.deep_symbolize_keys.
|
26
|
+
expect(nested_mixed.deep_symbolize_keys).to eq(nested_symbols)
|
27
27
|
end
|
28
28
|
|
29
29
|
it 'preserves fixnum keys' do
|
30
|
-
nested_fixnums.deep_symbolize_keys.
|
30
|
+
expect(nested_fixnums.deep_symbolize_keys).to eq(nested_fixnums)
|
31
31
|
end
|
32
32
|
|
33
33
|
it 'preserves keys that cannot be symbolized' do
|
34
|
-
nested_illegal_symbols.deep_symbolize_keys.
|
34
|
+
expect(nested_illegal_symbols.deep_symbolize_keys).to eq(nested_illegal_symbols)
|
35
35
|
end
|
36
36
|
end
|
@@ -10,8 +10,8 @@ describe Grocer::FailedDeliveryAttempt do
|
|
10
10
|
describe 'decoding' do
|
11
11
|
it 'accepts a binary tuple and sets each attribute' do
|
12
12
|
failed_delivery_attempt = described_class.new(binary_tuple)
|
13
|
-
failed_delivery_attempt.timestamp.
|
14
|
-
failed_delivery_attempt.device_token.
|
13
|
+
expect(failed_delivery_attempt.timestamp).to eq(timestamp)
|
14
|
+
expect(failed_delivery_attempt.device_token).to eq(device_token)
|
15
15
|
end
|
16
16
|
|
17
17
|
it 'raises an exception when there are problems decoding' do
|
@@ -17,45 +17,45 @@ describe Grocer::FeedbackConnection do
|
|
17
17
|
end
|
18
18
|
|
19
19
|
it 'can be initialized with a certificate' do
|
20
|
-
subject.certificate.
|
20
|
+
expect(subject.certificate).to eq('/path/to/cert.pem')
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'can be initialized with a passphrase' do
|
24
24
|
options[:passphrase] = 'open sesame'
|
25
|
-
subject.passphrase.
|
25
|
+
expect(subject.passphrase).to eq('open sesame')
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'defaults to Apple feedback gateway in production environment' do
|
29
29
|
Grocer.stubs(:env).returns('production')
|
30
|
-
subject.gateway.
|
30
|
+
expect(subject.gateway).to eq('feedback.push.apple.com')
|
31
31
|
end
|
32
32
|
|
33
33
|
it 'defaults to the sandboxed Apple feedback gateway in development environment' do
|
34
34
|
Grocer.stubs(:env).returns('development')
|
35
|
-
subject.gateway.
|
35
|
+
expect(subject.gateway).to eq('feedback.sandbox.push.apple.com')
|
36
36
|
end
|
37
37
|
|
38
38
|
it 'defaults to the sandboxed Apple feedback gateway in test environment' do
|
39
39
|
Grocer.stubs(:env).returns('test')
|
40
|
-
subject.gateway.
|
40
|
+
expect(subject.gateway).to eq('feedback.sandbox.push.apple.com')
|
41
41
|
end
|
42
42
|
|
43
43
|
it 'defaults to the sandboxed Apple feedback gateway for other random values' do
|
44
44
|
Grocer.stubs(:env).returns('random')
|
45
|
-
subject.gateway.
|
45
|
+
expect(subject.gateway).to eq('feedback.sandbox.push.apple.com')
|
46
46
|
end
|
47
47
|
|
48
48
|
it 'can be initialized with a gateway' do
|
49
49
|
options[:gateway] = 'gateway.example.com'
|
50
|
-
subject.gateway.
|
50
|
+
expect(subject.gateway).to eq('gateway.example.com')
|
51
51
|
end
|
52
52
|
|
53
53
|
it 'defaults to 2196 as the port' do
|
54
|
-
subject.port.
|
54
|
+
expect(subject.port).to eq(2196)
|
55
55
|
end
|
56
56
|
|
57
57
|
it 'can be initialized with a port' do
|
58
58
|
options[:port] = 443
|
59
|
-
subject.port.
|
59
|
+
expect(subject.port).to eq(443)
|
60
60
|
end
|
61
61
|
end
|
@@ -21,7 +21,7 @@ describe Grocer::Feedback do
|
|
21
21
|
subject { described_class.new(connection) }
|
22
22
|
|
23
23
|
it 'is enumerable' do
|
24
|
-
subject.
|
24
|
+
expect(subject).to be_kind_of(Enumerable)
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'reads failed delivery attempt messages from the connection' do
|
@@ -29,10 +29,10 @@ describe Grocer::Feedback do
|
|
29
29
|
|
30
30
|
delivery_attempts = subject.to_a
|
31
31
|
|
32
|
-
delivery_attempts[0].timestamp.
|
33
|
-
delivery_attempts[0].device_token.
|
32
|
+
expect(delivery_attempts[0].timestamp).to eq(jan1)
|
33
|
+
expect(delivery_attempts[0].device_token).to eq(device_token)
|
34
34
|
|
35
|
-
delivery_attempts[1].timestamp.
|
36
|
-
delivery_attempts[1].device_token.
|
35
|
+
expect(delivery_attempts[1].timestamp).to eq(jan2)
|
36
|
+
expect(delivery_attempts[1].device_token).to eq(device_token)
|
37
37
|
end
|
38
38
|
end
|
@@ -12,7 +12,7 @@ describe Grocer::NotificationReader do
|
|
12
12
|
io.rewind
|
13
13
|
|
14
14
|
notification = subject.first
|
15
|
-
notification.identifier.
|
15
|
+
expect(notification.identifier).to eq(1234)
|
16
16
|
end
|
17
17
|
|
18
18
|
it "reads expiry" do
|
@@ -20,7 +20,7 @@ describe Grocer::NotificationReader do
|
|
20
20
|
io.rewind
|
21
21
|
|
22
22
|
notification = subject.first
|
23
|
-
notification.expiry.
|
23
|
+
expect(notification.expiry).to eq(Time.utc(2013, 3, 24))
|
24
24
|
end
|
25
25
|
|
26
26
|
it "reads device token" do
|
@@ -28,7 +28,7 @@ describe Grocer::NotificationReader do
|
|
28
28
|
io.rewind
|
29
29
|
|
30
30
|
notification = subject.first
|
31
|
-
notification.device_token.
|
31
|
+
expect(notification.device_token).to eq('fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2')
|
32
32
|
end
|
33
33
|
|
34
34
|
it "reads alert" do
|
@@ -36,7 +36,7 @@ describe Grocer::NotificationReader do
|
|
36
36
|
io.rewind
|
37
37
|
|
38
38
|
notification = subject.first
|
39
|
-
notification.alert.
|
39
|
+
expect(notification.alert).to eq("Foo")
|
40
40
|
end
|
41
41
|
|
42
42
|
it "reads badge" do
|
@@ -44,7 +44,7 @@ describe Grocer::NotificationReader do
|
|
44
44
|
io.rewind
|
45
45
|
|
46
46
|
notification = subject.first
|
47
|
-
notification.badge.
|
47
|
+
expect(notification.badge).to eq(5)
|
48
48
|
end
|
49
49
|
|
50
50
|
it "reads sound" do
|
@@ -52,7 +52,7 @@ describe Grocer::NotificationReader do
|
|
52
52
|
io.rewind
|
53
53
|
|
54
54
|
notification = subject.first
|
55
|
-
notification.sound.
|
55
|
+
expect(notification.sound).to eq("foo.aiff")
|
56
56
|
end
|
57
57
|
|
58
58
|
it "reads custom attributes" do
|
@@ -60,7 +60,7 @@ describe Grocer::NotificationReader do
|
|
60
60
|
io.rewind
|
61
61
|
|
62
62
|
notification = subject.first
|
63
|
-
notification.custom.
|
63
|
+
expect(notification.custom).to eq({ foo: "bar" })
|
64
64
|
end
|
65
65
|
end
|
66
66
|
end
|
@@ -1,86 +1,49 @@
|
|
1
1
|
# encoding: UTF-8
|
2
|
-
|
3
2
|
require 'spec_helper'
|
4
3
|
require 'grocer/notification'
|
4
|
+
require 'grocer/shared_examples_for_notifications'
|
5
5
|
|
6
6
|
describe Grocer::Notification do
|
7
7
|
describe 'binary format' do
|
8
|
-
let(:notification) { described_class.new(payload_options) }
|
9
|
-
let(:payload_from_bytes) { notification.to_bytes[45..-1] }
|
10
|
-
let(:payload_dictionary_from_bytes) { JSON.parse(payload_from_bytes, symbolize_names: true) }
|
11
8
|
let(:payload_options) { { alert: 'hi', badge: 2, sound: 'siren.aiff' } }
|
9
|
+
let(:payload_dictionary_from_bytes) { JSON.parse(payload_from_bytes, symbolize_names: true) }
|
10
|
+
let(:payload_from_bytes) { notification.to_bytes[45..-1] }
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
it 'sets the command byte to 1' do
|
16
|
-
bytes[0].should == "\x01"
|
17
|
-
end
|
18
|
-
|
19
|
-
it 'defaults the identifer to 0' do
|
20
|
-
bytes[1...5].should == "\x00\x00\x00\x00"
|
21
|
-
end
|
22
|
-
|
23
|
-
it 'allows the identifier to be set' do
|
24
|
-
notification.identifier = 1234
|
25
|
-
bytes[1...5].should == [1234].pack('N')
|
26
|
-
end
|
27
|
-
|
28
|
-
it 'defaults expiry to zero' do
|
29
|
-
bytes[5...9].should == "\x00\x00\x00\x00"
|
30
|
-
end
|
31
|
-
|
32
|
-
it 'allows the expiry to be set' do
|
33
|
-
expiry = notification.expiry = Time.utc(2013, 3, 24, 12, 34, 56)
|
34
|
-
bytes[5...9].should == [expiry.to_i].pack('N')
|
35
|
-
end
|
36
|
-
|
37
|
-
it 'encodes the device token length as 32' do
|
38
|
-
bytes[9...11].should == "\x00\x20"
|
39
|
-
end
|
40
|
-
|
41
|
-
it 'encodes the device token as a 256-bit integer' do
|
42
|
-
token = notification.device_token = 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'
|
43
|
-
bytes[11...43].should == ['fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'].pack('H*')
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'as a convenience, flattens the device token to remove spaces' do
|
47
|
-
token = notification.device_token = 'fe15 a27d 5df3c3 4778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'
|
48
|
-
bytes[11...43].should == ['fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'].pack('H*')
|
49
|
-
end
|
50
|
-
|
51
|
-
it 'encodes the payload length' do
|
52
|
-
notification.alert = 'Hello World!'
|
53
|
-
bytes[43...45].should == [payload_from_bytes.bytesize].pack('n')
|
54
|
-
end
|
12
|
+
include_examples 'a notification'
|
55
13
|
|
56
14
|
it 'encodes alert as part of the payload' do
|
57
15
|
notification.alert = 'Hello World!'
|
58
|
-
payload_dictionary_from_bytes[:aps][:alert].
|
16
|
+
expect(payload_dictionary_from_bytes[:aps][:alert]).to eq('Hello World!')
|
59
17
|
end
|
60
18
|
|
61
19
|
it 'encodes badge as part of the payload' do
|
62
20
|
notification.badge = 42
|
63
|
-
payload_dictionary_from_bytes[:aps][:badge].
|
21
|
+
expect(payload_dictionary_from_bytes[:aps][:badge]).to eq(42)
|
64
22
|
end
|
65
23
|
|
66
24
|
it 'encodes sound as part of the payload' do
|
67
25
|
notification.sound = 'siren.aiff'
|
68
|
-
payload_dictionary_from_bytes[:aps][:sound].
|
26
|
+
expect(payload_dictionary_from_bytes[:aps][:sound]).to eq('siren.aiff')
|
69
27
|
end
|
70
28
|
|
71
29
|
it 'encodes custom payload attributes' do
|
72
30
|
notification.custom = { :foo => 'bar' }
|
73
|
-
payload_dictionary_from_bytes[:foo].
|
31
|
+
expect(payload_dictionary_from_bytes[:foo]).to eq('bar')
|
74
32
|
end
|
75
33
|
|
76
34
|
it 'encodes UTF-8 characters' do
|
77
35
|
notification.alert = '私'
|
78
|
-
payload_dictionary_from_bytes[:aps][:alert].force_encoding("UTF-8").
|
36
|
+
expect(payload_dictionary_from_bytes[:aps][:alert].force_encoding("UTF-8")).to eq('私')
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'encodes the payload length' do
|
40
|
+
notification.alert = 'Hello World!'
|
41
|
+
expect(bytes[43...45]).to eq([payload_from_bytes.bytesize].pack('n'))
|
79
42
|
end
|
80
43
|
|
81
44
|
it 'encodes the payload length correctly for multibyte UTF-8 strings' do
|
82
45
|
notification.alert = '私'
|
83
|
-
bytes[43...45].
|
46
|
+
expect(bytes[43...45]).to eq([payload_from_bytes.bytesize].pack('n'))
|
84
47
|
end
|
85
48
|
|
86
49
|
context 'invalid payload' do
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grocer/passbook_notification'
|
3
|
+
require 'grocer/shared_examples_for_notifications'
|
4
|
+
|
5
|
+
describe Grocer::PassbookNotification do
|
6
|
+
describe 'binary format' do
|
7
|
+
let(:payload_options) { Hash.new }
|
8
|
+
let(:payload_dictionary_from_bytes) { JSON.parse(payload_from_bytes, symbolize_names: true) }
|
9
|
+
let(:payload_from_bytes) { notification.to_bytes[45..-1] }
|
10
|
+
|
11
|
+
include_examples 'a notification'
|
12
|
+
|
13
|
+
it 'does not require a payload' do
|
14
|
+
expect(payload_dictionary_from_bytes[:aps]).to be_empty
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'encodes the payload length' do
|
18
|
+
payload_length = bytes[43...45].to_i
|
19
|
+
expect(payload_length).to be_zero
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -17,46 +17,51 @@ describe Grocer::PushConnection do
|
|
17
17
|
end
|
18
18
|
|
19
19
|
it 'can be initialized with a certificate' do
|
20
|
-
subject.certificate.
|
20
|
+
expect(subject.certificate).to eq('/path/to/cert.pem')
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'can be initialized with a passphrase' do
|
24
24
|
options[:passphrase] = 'open sesame'
|
25
|
-
subject.passphrase.
|
25
|
+
expect(subject.passphrase).to eq('open sesame')
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'defaults to Apple push gateway in production environment' do
|
29
29
|
Grocer.stubs(:env).returns('production')
|
30
|
-
subject.gateway.
|
30
|
+
expect(subject.gateway).to eq('gateway.push.apple.com')
|
31
31
|
end
|
32
32
|
|
33
33
|
it 'defaults to the sandboxed Apple push gateway in development environment' do
|
34
34
|
Grocer.stubs(:env).returns('development')
|
35
|
-
subject.gateway.
|
35
|
+
expect(subject.gateway).to eq('gateway.sandbox.push.apple.com')
|
36
36
|
end
|
37
37
|
|
38
38
|
it 'defaults to the localhost Apple push gateway in test environment' do
|
39
39
|
Grocer.stubs(:env).returns('test')
|
40
|
-
subject.gateway.
|
40
|
+
expect(subject.gateway).to eq('127.0.0.1')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'uses a case-insensitive environment to determine the push gateway' do
|
44
|
+
Grocer.stubs(:env).returns('TEST')
|
45
|
+
expect(subject.gateway).to eq('127.0.0.1')
|
41
46
|
end
|
42
47
|
|
43
48
|
it 'defaults to the sandboxed Apple push gateway for other random values' do
|
44
49
|
Grocer.stubs(:env).returns('random')
|
45
|
-
subject.gateway.
|
50
|
+
expect(subject.gateway).to eq('gateway.sandbox.push.apple.com')
|
46
51
|
end
|
47
52
|
|
48
53
|
it 'can be initialized with a gateway' do
|
49
54
|
options[:gateway] = 'gateway.example.com'
|
50
|
-
subject.gateway.
|
55
|
+
expect(subject.gateway).to eq('gateway.example.com')
|
51
56
|
end
|
52
57
|
|
53
58
|
it 'defaults to 2195 as the port' do
|
54
|
-
subject.port.
|
59
|
+
expect(subject.port).to eq(2195)
|
55
60
|
end
|
56
61
|
|
57
62
|
it 'can be initialized with a port' do
|
58
63
|
options[:port] = 443
|
59
|
-
subject.port.
|
64
|
+
expect(subject.port).to eq(443)
|
60
65
|
end
|
61
66
|
|
62
67
|
end
|
data/spec/grocer/server_spec.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
shared_examples 'a notification' do
|
2
|
+
let(:notification) { described_class.new(payload_options) }
|
3
|
+
|
4
|
+
subject(:bytes) { notification.to_bytes }
|
5
|
+
|
6
|
+
it 'sets the command byte to 1' do
|
7
|
+
expect(bytes[0]).to eq("\x01")
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'defaults the identifer to 0' do
|
11
|
+
expect(bytes[1...5]).to eq("\x00\x00\x00\x00")
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'allows the identifier to be set' do
|
15
|
+
notification.identifier = 1234
|
16
|
+
expect(bytes[1...5]).to eq([1234].pack('N'))
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'defaults expiry to zero' do
|
20
|
+
expect(bytes[5...9]).to eq("\x00\x00\x00\x00")
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'allows the expiry to be set' do
|
24
|
+
expiry = notification.expiry = Time.utc(2013, 3, 24, 12, 34, 56)
|
25
|
+
expect(bytes[5...9]).to eq([expiry.to_i].pack('N'))
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'encodes the device token length as 32' do
|
29
|
+
expect(bytes[9...11]).to eq("\x00\x20")
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'encodes the device token as a 256-bit integer' do
|
33
|
+
token = notification.device_token = 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'
|
34
|
+
expect(bytes[11...43]).to eq(['fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'].pack('H*'))
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'as a convenience, flattens the device token to remove spaces' do
|
38
|
+
token = notification.device_token = 'fe15 a27d 5df3c3 4778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'
|
39
|
+
expect(bytes[11...43]).to eq(['fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2'].pack('H*'))
|
40
|
+
end
|
41
|
+
end
|
@@ -35,7 +35,7 @@ describe Grocer::SSLConnection do
|
|
35
35
|
}
|
36
36
|
|
37
37
|
it 'is initialized with a certificate IO' do
|
38
|
-
subject.certificate.
|
38
|
+
expect(subject.certificate).to eq(File.read(connection_options[:certificate]))
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
@@ -43,19 +43,19 @@ describe Grocer::SSLConnection do
|
|
43
43
|
|
44
44
|
describe 'configuration' do
|
45
45
|
it 'is initialized with a certificate' do
|
46
|
-
subject.certificate.
|
46
|
+
expect(subject.certificate).to eq(connection_options[:certificate])
|
47
47
|
end
|
48
48
|
|
49
49
|
it 'is initialized with a passphrase' do
|
50
|
-
subject.passphrase.
|
50
|
+
expect(subject.passphrase).to eq(connection_options[:passphrase])
|
51
51
|
end
|
52
52
|
|
53
53
|
it 'is initialized with a gateway' do
|
54
|
-
subject.gateway.
|
54
|
+
expect(subject.gateway).to eq(connection_options[:gateway])
|
55
55
|
end
|
56
56
|
|
57
57
|
it 'is initialized with a port' do
|
58
|
-
subject.port.
|
58
|
+
expect(subject.port).to eq(connection_options[:port])
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
@@ -16,7 +16,7 @@ describe Grocer::SSLServer do
|
|
16
16
|
end
|
17
17
|
|
18
18
|
it "is constructed with a port option" do
|
19
|
-
subject.port.
|
19
|
+
expect(subject.port).to eq(12345)
|
20
20
|
end
|
21
21
|
|
22
22
|
|
@@ -25,7 +25,7 @@ describe Grocer::SSLServer do
|
|
25
25
|
clients = []
|
26
26
|
subject.accept { |c| clients << c }
|
27
27
|
|
28
|
-
clients.
|
28
|
+
expect(clients).to eq([mock_client])
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
data/spec/grocer_spec.rb
CHANGED
@@ -12,17 +12,17 @@ describe Grocer do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
it 'defaults to development' do
|
15
|
-
subject.env.
|
15
|
+
expect(subject.env).to eq('development')
|
16
16
|
end
|
17
17
|
|
18
18
|
it 'reads RAILS_ENV from ENV' do
|
19
19
|
ENV.stubs(:[]).with('RAILS_ENV').returns('staging')
|
20
|
-
subject.env.
|
20
|
+
expect(subject.env).to eq('staging')
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'reads RACK_ENV from ENV' do
|
24
24
|
ENV.stubs(:[]).with('RACK_ENV').returns('staging')
|
25
|
-
subject.env.
|
25
|
+
expect(subject.env).to eq('staging')
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
@@ -35,7 +35,7 @@ describe Grocer do
|
|
35
35
|
end
|
36
36
|
|
37
37
|
it 'gets a Pusher' do
|
38
|
-
subject.pusher(connection_options).
|
38
|
+
expect(subject.pusher(connection_options)).to be_a Grocer::Pusher
|
39
39
|
end
|
40
40
|
|
41
41
|
it 'passes the connection options on to the underlying Connection' do
|
@@ -50,7 +50,7 @@ describe Grocer do
|
|
50
50
|
end
|
51
51
|
|
52
52
|
it 'gets Feedback' do
|
53
|
-
subject.feedback(connection_options).
|
53
|
+
expect(subject.feedback(connection_options)).to be_a Grocer::Feedback
|
54
54
|
end
|
55
55
|
|
56
56
|
it 'passes the connection options on to the underlying Connection' do
|
@@ -66,7 +66,7 @@ describe Grocer do
|
|
66
66
|
end
|
67
67
|
|
68
68
|
it 'gets Server' do
|
69
|
-
subject.server(connection_options).
|
69
|
+
expect(subject.server(connection_options)).to be_a Grocer::Server
|
70
70
|
end
|
71
71
|
|
72
72
|
it 'passes the connection options on to the underlying SSLServer' do
|
@@ -76,5 +76,4 @@ describe Grocer do
|
|
76
76
|
end
|
77
77
|
|
78
78
|
end
|
79
|
-
|
80
79
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: grocer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.0
|
5
4
|
prerelease:
|
5
|
+
version: 0.3.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Andy Lindeman
|
@@ -11,104 +11,88 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2012-
|
14
|
+
date: 2012-12-22 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
|
-
name: json
|
18
|
-
requirement: !ruby/object:Gem::Requirement
|
19
|
-
none: false
|
20
|
-
requirements:
|
21
|
-
- - ! '>='
|
22
|
-
- !ruby/object:Gem::Version
|
23
|
-
version: '0'
|
24
|
-
type: :runtime
|
25
|
-
prerelease: false
|
26
17
|
version_requirements: !ruby/object:Gem::Requirement
|
27
|
-
none: false
|
28
|
-
requirements:
|
29
|
-
- - ! '>='
|
30
|
-
- !ruby/object:Gem::Version
|
31
|
-
version: '0'
|
32
|
-
- !ruby/object:Gem::Dependency
|
33
|
-
name: rspec
|
34
|
-
requirement: !ruby/object:Gem::Requirement
|
35
|
-
none: false
|
36
18
|
requirements:
|
37
19
|
- - ~>
|
38
20
|
- !ruby/object:Gem::Version
|
39
21
|
version: '2.11'
|
22
|
+
none: false
|
23
|
+
name: rspec
|
40
24
|
type: :development
|
41
25
|
prerelease: false
|
42
|
-
|
43
|
-
none: false
|
26
|
+
requirement: !ruby/object:Gem::Requirement
|
44
27
|
requirements:
|
45
28
|
- - ~>
|
46
29
|
- !ruby/object:Gem::Version
|
47
30
|
version: '2.11'
|
48
|
-
- !ruby/object:Gem::Dependency
|
49
|
-
name: pry
|
50
|
-
requirement: !ruby/object:Gem::Requirement
|
51
31
|
none: false
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
34
|
requirements:
|
53
35
|
- - ~>
|
54
36
|
- !ruby/object:Gem::Version
|
55
37
|
version: 0.9.8
|
38
|
+
none: false
|
39
|
+
name: pry
|
56
40
|
type: :development
|
57
41
|
prerelease: false
|
58
|
-
|
59
|
-
none: false
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
60
43
|
requirements:
|
61
44
|
- - ~>
|
62
45
|
- !ruby/object:Gem::Version
|
63
46
|
version: 0.9.8
|
64
|
-
- !ruby/object:Gem::Dependency
|
65
|
-
name: mocha
|
66
|
-
requirement: !ruby/object:Gem::Requirement
|
67
47
|
none: false
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
50
|
requirements:
|
69
51
|
- - ! '>='
|
70
52
|
- !ruby/object:Gem::Version
|
71
53
|
version: '0'
|
54
|
+
none: false
|
55
|
+
name: mocha
|
72
56
|
type: :development
|
73
57
|
prerelease: false
|
74
|
-
|
75
|
-
none: false
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
76
59
|
requirements:
|
77
60
|
- - ! '>='
|
78
61
|
- !ruby/object:Gem::Version
|
79
62
|
version: '0'
|
80
|
-
- !ruby/object:Gem::Dependency
|
81
|
-
name: bourne
|
82
|
-
requirement: !ruby/object:Gem::Requirement
|
83
63
|
none: false
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
84
66
|
requirements:
|
85
67
|
- - ! '>='
|
86
68
|
- !ruby/object:Gem::Version
|
87
69
|
version: '0'
|
70
|
+
none: false
|
71
|
+
name: bourne
|
88
72
|
type: :development
|
89
73
|
prerelease: false
|
90
|
-
|
91
|
-
none: false
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
92
75
|
requirements:
|
93
76
|
- - ! '>='
|
94
77
|
- !ruby/object:Gem::Version
|
95
78
|
version: '0'
|
96
|
-
- !ruby/object:Gem::Dependency
|
97
|
-
name: rake
|
98
|
-
requirement: !ruby/object:Gem::Requirement
|
99
79
|
none: false
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
82
|
requirements:
|
101
83
|
- - ! '>='
|
102
84
|
- !ruby/object:Gem::Version
|
103
85
|
version: '0'
|
86
|
+
none: false
|
87
|
+
name: rake
|
104
88
|
type: :development
|
105
89
|
prerelease: false
|
106
|
-
|
107
|
-
none: false
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
108
91
|
requirements:
|
109
92
|
- - ! '>='
|
110
93
|
- !ruby/object:Gem::Version
|
111
94
|
version: '0'
|
95
|
+
none: false
|
112
96
|
description: ! " Grocer interfaces with the Apple Push\n
|
113
97
|
\ Notification Service to send push\n notifications
|
114
98
|
to iOS devices and collect\n notification feedback via
|
@@ -145,6 +129,7 @@ files:
|
|
145
129
|
- lib/grocer/no_port_error.rb
|
146
130
|
- lib/grocer/notification.rb
|
147
131
|
- lib/grocer/notification_reader.rb
|
132
|
+
- lib/grocer/passbook_notification.rb
|
148
133
|
- lib/grocer/push_connection.rb
|
149
134
|
- lib/grocer/pusher.rb
|
150
135
|
- lib/grocer/server.rb
|
@@ -164,9 +149,11 @@ files:
|
|
164
149
|
- spec/grocer/feedback_spec.rb
|
165
150
|
- spec/grocer/notification_reader_spec.rb
|
166
151
|
- spec/grocer/notification_spec.rb
|
152
|
+
- spec/grocer/passbook_notification_spec.rb
|
167
153
|
- spec/grocer/push_connection_spec.rb
|
168
154
|
- spec/grocer/pusher_spec.rb
|
169
155
|
- spec/grocer/server_spec.rb
|
156
|
+
- spec/grocer/shared_examples_for_notifications.rb
|
170
157
|
- spec/grocer/ssl_connection_spec.rb
|
171
158
|
- spec/grocer/ssl_server_spec.rb
|
172
159
|
- spec/grocer_spec.rb
|
@@ -179,20 +166,20 @@ rdoc_options: []
|
|
179
166
|
require_paths:
|
180
167
|
- lib
|
181
168
|
required_ruby_version: !ruby/object:Gem::Requirement
|
182
|
-
none: false
|
183
169
|
requirements:
|
184
170
|
- - ! '>='
|
185
171
|
- !ruby/object:Gem::Version
|
186
172
|
version: '0'
|
187
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
188
173
|
none: false
|
174
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
189
175
|
requirements:
|
190
176
|
- - ! '>='
|
191
177
|
- !ruby/object:Gem::Version
|
192
178
|
version: '0'
|
179
|
+
none: false
|
193
180
|
requirements: []
|
194
181
|
rubyforge_project:
|
195
|
-
rubygems_version: 1.8.
|
182
|
+
rubygems_version: 1.8.23
|
196
183
|
signing_key:
|
197
184
|
specification_version: 3
|
198
185
|
summary: Pushing Apple notifications since 2012.
|
@@ -207,10 +194,13 @@ test_files:
|
|
207
194
|
- spec/grocer/feedback_spec.rb
|
208
195
|
- spec/grocer/notification_reader_spec.rb
|
209
196
|
- spec/grocer/notification_spec.rb
|
197
|
+
- spec/grocer/passbook_notification_spec.rb
|
210
198
|
- spec/grocer/push_connection_spec.rb
|
211
199
|
- spec/grocer/pusher_spec.rb
|
212
200
|
- spec/grocer/server_spec.rb
|
201
|
+
- spec/grocer/shared_examples_for_notifications.rb
|
213
202
|
- spec/grocer/ssl_connection_spec.rb
|
214
203
|
- spec/grocer/ssl_server_spec.rb
|
215
204
|
- spec/grocer_spec.rb
|
216
205
|
- spec/spec_helper.rb
|
206
|
+
has_rdoc:
|