Skip to content

Commit

Permalink
Skeleton, started working on specs for it.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sutto committed Jun 16, 2012
0 parents commit 68276ed
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
/spec/certificates/us.pem
Gemfile.lock
.test-env
2 changes: 2 additions & 0 deletions .rspec
@@ -0,0 +1,2 @@
--colour
--format=documentation
3 changes: 3 additions & 0 deletions Gemfile
@@ -0,0 +1,3 @@
source "https://rubygems.org"

gemspec
10 changes: 10 additions & 0 deletions Rakefile
@@ -0,0 +1,10 @@
require 'rubygems'
require 'rake'
require 'rspec/core'
require 'rspec/core/rake_task'
require 'bundler/gem_tasks'

desc "Run all specs in spec directory (excluding plugin specs)"
RSpec::Core::RakeTask.new(:spec)

task :default => :spec
133 changes: 133 additions & 0 deletions lib/pyapns2.rb
@@ -0,0 +1,133 @@
require 'net/http'
require 'xml/libxml/xmlrpc'

class Pyapns2

require 'pyapns2/version'

class Error < StandardError; end

attr_reader :host, :port

class ProvisionedClient

attr_reader :client, :app_id

def initialize(client, app_id)
@client = client
@app_id = app_id
end

# See Pyapns2::Client#notify, with the exception this version prefills in the app_id.
def notify(token, notification = nil)
client.notify app_id, token, notification
end

# See Pyapns2::Client#feedback, with the exception this version prefills in the app_id.
def feedback
client.feedback app_id
end

def inspect
"#<#{self.class.name} server=#{host}:#{port}, app_id=#{app_id}>"
end

end

# Returns a pre-provisioned client that also automatically prepends
# the app_id automatically to all api calls, making giving a simpler interface.
def self.provision(options = {})
host, port = options.delete(:host), options.delete(:port)
host ||= "localhost"
port ||= 7077
client = new(host, port)
client.provision(options)
ProvisionedClient.new client, options[:app_id]
end

def initialize(host = 'localhost', port = 7077)
raise ArgumentError, "host must be a string" unless host.is_a?(String)
raise ArgumentError, "port must be a number" unless port.is_a?(Numeric)
@host = host
@port = port
@http = Net::HTTP.new host, port
@xmlrpc = XML::XMLRPC::Client.new @http, "/"
end

def inspect
"#<#{self.class.name} server=#{host}:#{port}>"
end

# Given a hash of options, calls provision on the pyapns server. This
# expects the following options and will raise an ArgumentError if they're
# not given:
#
# :app_id - A String name for your application
# :timeout - A number (e.g. 15) for how long to time out after when connecting to the apn server
# :env / :environment - One of production or sandbox. The type of server to connect to.
# :cert - Either a path to the certificate file or the certificate contents as a string.
def provision(options)
options[:environment] = options.delete(:env) if options.has_key?(:env)
app_id = options[:app_id]
timeout = options[:timeout]
cert = options[:cert]
env = options[:environment]
raise ArgumentError, ":app_id must be a string" unless app_id.is_a?(String)
raise ArgumentError, ":cert must be a string" unless cert.is_a?(String)
raise ArgumentError, ":environment (or :env) must be one of sandbox or production" unless %w(production sandbox).include?(env)
raise ArgumentError, ":timeout must be a valid integer" unless timeout.is_a?(Numeric) && timeout >= 0
@xmlrpc.call 'provision', app_id, cert, env, timeout
true
rescue LibXML::XML::XMLRPC::RemoteCallError => e
raise Error.new e.message
end

# The main notification endpoint. Takes the app_id as the first argument, and then one
# of three sets of notification data:
#
# 1. A single token (as a string) and notification (as a dictionary)
# 2. A hash of token to notifications.
# 3. An array of tokens mapping to an array of notifications.
#
# Under the hook, it will automatically convert it to the most appropriate form before continuing.
# Will raise ArgumentError if you attempt to pass in bad information.
def notify(app_id, token, notification = nil)
if token.is_a?(Hash)
token, notification = extra_notification_info_from_hash token
end
raise ArgumentError, "Please ensure you provide an app_id" unless app_id
raise ArgumentError, "Please ensure you provide a single notification or an array of notifications" unless typed_item_of(notification, Hash)
raise ArgumentError, "Please ensure you provide device tokens or a string of tokens" unless typed_item_of(token, String)
types = [notification.is_a?(Array), token.is_a?(Array)]
if types.any? && !types.all?
raise ArgumentError, "The notifications and the strings must both be arrays if one is."
end
@xmlrpc.call 'notify', app_id, token, notification
true
rescue LibXML::XML::XMLRPC::RemoteCallError => e
raise Error.new e.message
end

# Takes an app id and returns the list of feedback from pyapns.
def feedback(app_id)
@xmlrpc.call('feedback', app_id).params
rescue LibXML::XML::XMLRPC::RemoteCallError => e
raise Error.new e.message
end

private

def extra_notification_info_from_hash(hash)
tokens, notifications = [], []
hash.each_pair do |k,v|
tokens << k
notifications << v
end
return tokens, notifications
end

def typed_item_of(value, klass)
value.is_a?(klass) || (value.is_a?(Array) && value.all? { |v| v.is_a?(klass) })
end

end
3 changes: 3 additions & 0 deletions lib/pyapns2/version.rb
@@ -0,0 +1,3 @@
class Pyapns2
VERSION = "1.0.0"
end
27 changes: 27 additions & 0 deletions pyapns2.gemspec
@@ -0,0 +1,27 @@
# -*- encoding: utf-8 -*-
lib = File.expand_path('../lib/', __FILE__)
$:.unshift lib unless $:.include?(lib)

require 'pyapns2/version'

Gem::Specification.new do |s|
s.name = "pyapns2"
s.version = Pyapns2::VERSION
s.platform = Gem::Platform::RUBY
s.authors = ["Darcy Laycock"]
s.email = ["darcy@filtersquad.com"]
s.homepage = "http://github.com/filtersquad"
s.summary = "An alternative ruby client for the pyapns push notification server with an emphasis on Ruby 1.9 support."
s.description = "Pyapns2 provides an alterantive, simpler client for the pyapns push notification server, using libxml-xmlrpc to handle all of the xmlrpc.\nIt also is tested against Ruby 1.9"
s.required_rubygems_version = ">= 1.3.6"

s.add_dependency 'libxml-xmlrpc'
s.add_development_dependency 'rake'
s.add_development_dependency 'rspec', '~> 2.4'
s.add_development_dependency 'rr', '~> 1.0'
s.add_development_dependency 'webmock'
s.add_development_dependency 'vcr'

s.files = Dir.glob("{lib}/**/*")
s.require_path = 'lib'
end
1 change: 1 addition & 0 deletions spec/certificates/fake.pem
@@ -0,0 +1 @@
This is a fake certificate.
Empty file added spec/provisioned_client_spec.rb
Empty file.
74 changes: 74 additions & 0 deletions spec/pyapns2_spec.rb
@@ -0,0 +1,74 @@
require 'spec_helper'

describe Pyapns2 do

let(:config) { TestConfiguration.instance }

let(:options) { config.provisioning_options }
let(:client) { Pyapns2.new config.host, config.port }

describe 'provisioning' do

it 'should raise an argument error with an invalid app_id' do
expect { client.provision options.merge(app_id: nil) }.to raise_error ArgumentError
expect { client.provision options.merge(app_id: "") }.to raise_error ArgumentError
expect { client.provision options.merge(app_id: 123) }.to raise_error ArgumentError
end

it 'should raise an argument error with an invalid cert'

it 'should raise an argument error with an invalid timeout'

it 'should raise an argument error with an invalid environment'

context 'hitting the server' do

it 'should return true'

end

end

describe 'notifying a user' do

it 'should raise an error with an invalid app_id'

it 'should raise an error with invalid tokens'

it 'should raise an error with invalid notifications'

it 'should raise an error with mismatched types'

context 'hitting the server' do

it 'should raise an exception if notifications fail'

it 'should return true if the notification works'

it 'should work with a hash'

it 'should work with a single notification'

it 'should work with an array'

it 'should return the error with an unknown app_id'

end

end

describe 'getting feedback' do

it 'should raise an error with an invalid app_id'

context 'htting the server' do

it 'should correctly return an array'

it 'should return the error with an invalid app id'

end

end

end
2 changes: 2 additions & 0 deletions spec/run-pyapns
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
twistd --syslog -n web --class=pyapns.server.APNSServer --port=7077
55 changes: 55 additions & 0 deletions spec/spec_helper.rb
@@ -0,0 +1,55 @@
$LOAD_PATH.unshift Pathname(__FILE__).dirname.dirname.join("lib").to_s

require 'webmock'
require 'vcr'
require 'rr'

require 'pyapns2'

require 'singleton'
class TestConfiguration

include Singleton

attr_reader :host, :port, :token, :cert_path, :fake_app

def initialize
fake_token = (1..64).map { |t| rand(16).to_s(16) }.join
self.host = (ENV['PYAPNS_HOST'] || 'localhost')
self.port = (ENV['PYAPNS_PORT'] || 7077).to_i
self.token = (ENV['TEST_PUSH_TOKEN'] || fake_token)
self.cert_path = File.expand_path "../certificates/#{(ENV['PYAPNS_CERT'] || "fake")}.pem", __FILE__
self.fake_app = "fake-app-#{Time.now.to_i}"
end

def cert
@cert ||= File.read(cert_path)
end

def provisioning_options
{
app_id: app_id,
cert: cert,
environment: 'sandbox',
timeout: 15
}
end

end

p $pyapns_default_options

VCR.configure do |c|
c.cassette_library_dir = 'spec/cassettes'
c.hook_into :webmock
c.default_cassette_options = {:record => :new_episodes}
# Filter out parts of the test
config = TestConfiguration.instance
%w(pyapns_host pyapns_port notification_token pyapns_fake_app).each do |var_name|
c.filter_sensitive_data("{#{var_name}}") { config.send var_name }
end
end

RSpec.configure do |config|
config.mock_with :rr
end

0 comments on commit 68276ed

Please sign in to comment.