grocer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore 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