grocer 0.2.0 → 0.3.0
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/.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
|
-
[](https://travis-ci.org/highgroove/grocer)
|
|
4
4
|
[](https://gemnasium.com/highgroove/grocer)
|
|
5
5
|
[](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:
|