grocer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .DS_Store
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ tags
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format progress
3
+ -r pry
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - ruby-head
6
+ # - rbx-19mode
7
+ # - jruby-19mode
8
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in grocer.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Steven Harman
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # Grocer
2
+
3
+ [![Build Status](https://secure.travis-ci.org/highgroove/grocer.png)](http://travis-ci.org/highgroove/grocer)
4
+ [![Dependency Status](https://gemnasium.com/highgroove/grocer.png)](https://gemnasium.com/highgroove/grocer)
5
+
6
+ *grocer* interfaces with the [Apple Push Notification
7
+ Service](http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html)
8
+ to send push notifications to iOS devices.
9
+
10
+ There are other gems out there to do this, but *grocer* plans to be the
11
+ cleanest, most extensible, and best maintained.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'grocer'
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Connecting
24
+
25
+ ```ruby
26
+ # `certificate` is the only required option; the rest will default to the values
27
+ # shown here.
28
+ #
29
+ # Information on obtaining a `.pem` file for use with `certificate` is shown
30
+ # later.
31
+ pusher = Grocer.pusher.new(
32
+ certificate: "/path/to/cert.pem", # required
33
+ passphrase: "", # optional
34
+ gateway: "gateway.push.apple.com", # optional; See note below.
35
+ port: 2195 # optional
36
+ )
37
+ ```
38
+
39
+ **NOTE**: The `gateway` option defaults to `gateway.push.apple.com`
40
+ **only** when running in a production environement, as determined by
41
+ either the `RAILS_ENV` or `RACK_ENV` environment variables. In all other
42
+ cases, it defaults to the sandbox gateway,
43
+ `gateway.sandbox.push.apple.com`.
44
+
45
+ ### Sending Notifications
46
+
47
+ ```ruby
48
+ # `device_token` and either `alert` or `badge` are required.
49
+ #
50
+ # Information on obtaining `device_token` is shown later.
51
+ notification = Grocer::Notification.new(
52
+ device_token: "fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2",
53
+ alert: "Hello from Grocer!",
54
+ badge: 42,
55
+ sound: "siren.aiff", # optional
56
+ expiry: Time.now + 60*60, # optional; 0 is default, meaning the message is not stored
57
+ identifier: 1234 # optional
58
+ )
59
+
60
+ pusher.push(notification)
61
+ ```
62
+
63
+ It is desirable to reuse the same connection to send multiple notifications, as
64
+ is recommended by Apple.
65
+
66
+ ```ruby
67
+ pusher = Grocer.pusher(connection_options)
68
+ notifications.each do |notification|
69
+ pusher.push(notification)
70
+ end
71
+ ```
72
+
73
+ ### Feedback
74
+
75
+ ```ruby
76
+ # `certificate` is the only required option; the rest will default to the values
77
+ # shown here.
78
+ feedback = Grocer.feedback.new(
79
+ certificate: "/path/to/cert.pem", # required
80
+ passphrase: "", # optional
81
+ gateway: "feedback.push.apple.com", # optional; See note below.
82
+ port: 2196 # optional
83
+ )
84
+
85
+ feedback.each do |attempt|
86
+ puts "Device #{attempt.device_token} failed at #{attempt.timestamp}
87
+ end
88
+ ```
89
+
90
+ **NOTE**: The `gateway` option defaults to `feedback.push.apple.com`
91
+ **only** when running in a production environement, as determined by
92
+ either the `RAILS_ENV` or `RACK_ENV` environment variables. In all other
93
+ cases, it defaults to the sandbox gateway,
94
+ `feedback.sandbox.push.apple.com`.
95
+
96
+ ### Device Token
97
+
98
+ A device token is obtained from within the iOS app. More details are in Apple's
99
+ [Registering for Remote
100
+ Notifications](http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/IPhoneOSClientImp/IPhoneOSClientImp.html#//apple_ref/doc/uid/TP40008194-CH103-SW1)
101
+ documentation.
102
+
103
+ The key code for this purpose is:
104
+
105
+ ```objective-c
106
+ - (void)applicationDidFinishLaunching:(UIApplication *)app {
107
+ // other setup tasks here....
108
+ [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];
109
+ }
110
+
111
+ - (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {
112
+ NSLog(@"Got device token: %@", [devToken description]);
113
+
114
+ [self sendProviderDeviceToken:[devToken bytes]]; // custom method; e.g., send to a web service and store
115
+ }
116
+
117
+ - (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
118
+ NSLog(@"Error in registration. Error: %@", err);
119
+ }
120
+ ```
121
+
122
+ ### Certificate File
123
+
124
+ TODO: Describe how to get a .pem
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task :default => :spec
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,4 @@
1
+ module Grocer
2
+ class InvalidFormatError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Grocer
2
+ class NoCertificateError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Grocer
2
+ class NoGatewayError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Grocer
2
+ class NoPayloadError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Grocer
2
+ class NoPortError < StandardError
3
+ end
4
+ 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,11 @@
1
+ module Grocer
2
+ class Pusher
3
+ def initialize(connection)
4
+ @connection = connection
5
+ end
6
+
7
+ def push(notification)
8
+ @connection.write(notification.to_bytes)
9
+ end
10
+ end
11
+ 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,3 @@
1
+ module Grocer
2
+ VERSION = '0.0.1'
3
+ 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
@@ -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
@@ -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