pusher 0.3.5 → 0.4.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +8 -3
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/lib/pusher.rb +11 -9
- data/lib/pusher/authentication.rb +144 -0
- data/lib/pusher/channel.rb +53 -15
- data/pusher.gemspec +13 -4
- data/spec/authentication_spec.rb +176 -0
- data/spec/pusher_spec.rb +90 -30
- metadata +40 -10
data/README.md
CHANGED
@@ -6,13 +6,18 @@ Getting started
|
|
6
6
|
|
7
7
|
After registering at <http://pusherapp.com> configure your app with the security credentials
|
8
8
|
|
9
|
+
Pusher.app_id = 'your-pusher-app-id'
|
9
10
|
Pusher.key = 'your-pusher-key'
|
10
11
|
Pusher.secret = 'your-pusher-secret'
|
11
12
|
|
12
|
-
Trigger an event
|
13
|
+
Trigger an event. Channel and event names may only contain alphanumeric characters, '-' and '_'.
|
14
|
+
|
15
|
+
Pusher['a_channel'].trigger('an_event', {:some => 'data'})
|
16
|
+
|
17
|
+
Optionally a socket id may be provided. This will prevent the event from being triggered on this specific socket id (see <http://pusherapp.com/docs/duplicates> for more info).
|
18
|
+
|
19
|
+
Pusher['a_channel'].trigger('an_event', {:some => 'data'}, socket_id)
|
13
20
|
|
14
|
-
Pusher['arbitrary-channel-name'].trigger({:some => 'data'})
|
15
|
-
|
16
21
|
Logging
|
17
22
|
-------
|
18
23
|
|
data/Rakefile
CHANGED
@@ -12,7 +12,9 @@ begin
|
|
12
12
|
gem.authors = ["New Bamboo"]
|
13
13
|
# gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
14
|
gem.add_dependency "json"
|
15
|
+
gem.add_dependency "crack"
|
15
16
|
gem.add_development_dependency "rspec", ">= 1.2.9"
|
17
|
+
gem.add_development_dependency "webmock"
|
16
18
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
19
|
end
|
18
20
|
Jeweler::GemcutterTasks.new
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0.beta
|
data/lib/pusher.rb
CHANGED
@@ -5,16 +5,13 @@ require 'net/http'
|
|
5
5
|
autoload 'Logger', 'logger'
|
6
6
|
|
7
7
|
module Pusher
|
8
|
-
class
|
9
|
-
|
10
|
-
'You must configure both Pusher.key in order to authenticate your Pusher app'
|
11
|
-
end
|
12
|
-
end
|
8
|
+
class Error < RuntimeError; end
|
9
|
+
class AuthenticationError < Error; end
|
13
10
|
|
14
11
|
class << self
|
15
12
|
attr_accessor :host, :port
|
16
13
|
attr_writer :logger
|
17
|
-
attr_accessor :key, :secret
|
14
|
+
attr_accessor :app_id, :key, :secret
|
18
15
|
|
19
16
|
def logger
|
20
17
|
@logger ||= begin
|
@@ -23,16 +20,21 @@ module Pusher
|
|
23
20
|
log
|
24
21
|
end
|
25
22
|
end
|
23
|
+
|
24
|
+
def authentication_token
|
25
|
+
Authentication::Token.new(@key, @secret)
|
26
|
+
end
|
26
27
|
end
|
27
28
|
|
28
29
|
self.host = 'api.pusherapp.com'
|
29
30
|
self.port = 80
|
30
31
|
|
31
|
-
def self.[](
|
32
|
-
raise ArgumentError unless @key
|
32
|
+
def self.[](channel_name)
|
33
|
+
raise ArgumentError, 'Missing configuration: please check that Pusher.app_id, Pusher.key, and Pusher.secret are all configured' unless @app_id && @key && @secret
|
33
34
|
@channels ||= {}
|
34
|
-
@channels[
|
35
|
+
@channels[channel_name.to_s] = Channel.new(@app_id, channel_name)
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
38
39
|
require 'pusher/channel'
|
40
|
+
require 'pusher/authentication'
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'hmac-sha2'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Authentication
|
5
|
+
class AuthenticationError < RuntimeError; end
|
6
|
+
|
7
|
+
class Token
|
8
|
+
attr_reader :key, :secret
|
9
|
+
|
10
|
+
def initialize(key, secret)
|
11
|
+
@key, @secret = key, secret
|
12
|
+
end
|
13
|
+
|
14
|
+
def sign(request)
|
15
|
+
request.sign(self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Request
|
20
|
+
attr_accessor :path, :query_hash
|
21
|
+
|
22
|
+
# http://www.w3.org/TR/NOTE-datetime
|
23
|
+
ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
|
24
|
+
|
25
|
+
def initialize(method, path, query)
|
26
|
+
raise ArgumentError, "Expected string" unless path.kind_of?(String)
|
27
|
+
raise ArgumentError, "Expected hash" unless query.kind_of?(Hash)
|
28
|
+
|
29
|
+
query_hash = {}
|
30
|
+
auth_hash = {}
|
31
|
+
query.each do |key, v|
|
32
|
+
k = key.to_s.downcase
|
33
|
+
k[0..4] == 'auth_' ? auth_hash[k] = v : query_hash[k] = v
|
34
|
+
end
|
35
|
+
|
36
|
+
@method = method.upcase
|
37
|
+
@path, @query_hash, @auth_hash = path, query_hash, auth_hash
|
38
|
+
end
|
39
|
+
|
40
|
+
def sign(token)
|
41
|
+
@auth_hash = {
|
42
|
+
:auth_version => "1.0",
|
43
|
+
:auth_key => token.key,
|
44
|
+
:auth_timestamp => Time.now.to_i
|
45
|
+
}
|
46
|
+
|
47
|
+
hmac_signature = HMAC::SHA256.digest(token.secret, string_to_sign)
|
48
|
+
# chomp because the Base64 output ends with \n
|
49
|
+
@auth_hash[:auth_signature] = Base64.encode64(hmac_signature).chomp
|
50
|
+
|
51
|
+
return @auth_hash
|
52
|
+
end
|
53
|
+
|
54
|
+
# Authenticates the request with a token
|
55
|
+
#
|
56
|
+
# Timestamp check: Unless timestamp_grace is set to nil (which will skip
|
57
|
+
# the timestamp check), an exception will be raised if timestamp is not
|
58
|
+
# supplied or if the timestamp provided is not within timestamp_grace of
|
59
|
+
# the real time (defaults to 10 minutes)
|
60
|
+
#
|
61
|
+
# Signature check: Raises an exception if the signature does not match the
|
62
|
+
# computed value
|
63
|
+
#
|
64
|
+
def authenticate_by_token!(token, timestamp_grace = 600)
|
65
|
+
validate_version!
|
66
|
+
validate_timestamp!(timestamp_grace)
|
67
|
+
validate_signature!(token)
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
def authenticate_by_token(token, timestamp_grace = 600)
|
72
|
+
authenticate_by_token!(token, timestamp_grace)
|
73
|
+
rescue AuthenticationError
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
def authenticate(timestamp_grace = 600, &block)
|
78
|
+
key = @auth_hash['auth_key']
|
79
|
+
raise AuthenticationError, "Authentication key required" unless key
|
80
|
+
token = yield key
|
81
|
+
unless token && token.secret
|
82
|
+
raise AuthenticationError, "Invalid authentication key"
|
83
|
+
end
|
84
|
+
authenticate_by_token!(token, timestamp_grace)
|
85
|
+
return token
|
86
|
+
end
|
87
|
+
|
88
|
+
def auth_hash
|
89
|
+
raise "Request not signed" unless @auth_hash && @auth_hash[:auth_signature]
|
90
|
+
@auth_hash
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def string_to_sign
|
96
|
+
[@method, @path, parameter_string].join("\n")
|
97
|
+
end
|
98
|
+
|
99
|
+
def parameter_string
|
100
|
+
param_hash = @query_hash.merge(@auth_hash || {})
|
101
|
+
|
102
|
+
# Convert keys to lowercase strings
|
103
|
+
hash = {}; param_hash.each { |k,v| hash[k.to_s.downcase] = v }
|
104
|
+
|
105
|
+
# Exclude signature from signature generation!
|
106
|
+
hash.delete("auth_signature")
|
107
|
+
|
108
|
+
hash.keys.sort.map { |k| "#{k}=#{hash[k]}" }.join("&")
|
109
|
+
end
|
110
|
+
|
111
|
+
def validate_version!
|
112
|
+
version = @auth_hash["auth_version"]
|
113
|
+
raise AuthenticationError, "Version required" unless version
|
114
|
+
raise AuthenticationError, "Version not supported" unless version == '1.0'
|
115
|
+
end
|
116
|
+
|
117
|
+
def validate_timestamp!(grace)
|
118
|
+
return true if grace.nil?
|
119
|
+
|
120
|
+
timestamp = @auth_hash["auth_timestamp"]
|
121
|
+
error = (timestamp.to_i - Time.now.to_i).abs
|
122
|
+
raise AuthenticationError, "Timestamp required" unless timestamp
|
123
|
+
if error >= grace
|
124
|
+
raise AuthenticationError, "Timestamp expired: Given timestamp "\
|
125
|
+
"(#{Time.at(timestamp.to_i).utc.strftime(ISO8601)}) "\
|
126
|
+
"not within #{grace}s of server time "\
|
127
|
+
"(#{Time.now.utc.strftime(ISO8601)})"
|
128
|
+
end
|
129
|
+
return true
|
130
|
+
end
|
131
|
+
|
132
|
+
def validate_signature!(token)
|
133
|
+
string = string_to_sign
|
134
|
+
hmac_signature = HMAC::SHA256.digest(token.secret, string)
|
135
|
+
# chomp because the Base64 output ends with \n
|
136
|
+
base64_signature = Base64.encode64(hmac_signature).chomp
|
137
|
+
unless @auth_hash["auth_signature"] == base64_signature
|
138
|
+
raise AuthenticationError, "Invalid signature: you should have "\
|
139
|
+
"sent Base64Encode(HmacSHA256(#{string.inspect}, your_secret_key))"
|
140
|
+
end
|
141
|
+
return true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/pusher/channel.rb
CHANGED
@@ -1,24 +1,62 @@
|
|
1
|
+
require 'crack/core_extensions' # Used for Hash#to_params
|
2
|
+
require 'digest/md5'
|
3
|
+
|
1
4
|
module Pusher
|
2
5
|
class Channel
|
3
|
-
def initialize(
|
4
|
-
@uri = URI.
|
6
|
+
def initialize(app_id, name)
|
7
|
+
@uri = URI::HTTP.build({
|
8
|
+
:host => Pusher.host,
|
9
|
+
:port => Pusher.port,
|
10
|
+
:path => "/apps/#{app_id}/channels/#{name}/events"
|
11
|
+
})
|
5
12
|
@http = Net::HTTP.new(@uri.host, @uri.port)
|
6
13
|
end
|
7
14
|
|
8
|
-
def trigger(event_name, data, socket_id = nil)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
def trigger!(event_name, data, socket_id = nil)
|
16
|
+
params = {
|
17
|
+
:name => event_name,
|
18
|
+
}
|
19
|
+
params[:socket_id] = socket_id if socket_id
|
20
|
+
|
21
|
+
body = case data
|
22
|
+
when String
|
23
|
+
data
|
24
|
+
else
|
25
|
+
begin
|
26
|
+
self.class.turn_into_json(data)
|
27
|
+
rescue => e
|
28
|
+
Pusher.logger.error("Could not convert #{data.inspect} into JSON")
|
29
|
+
raise e
|
30
|
+
end
|
21
31
|
end
|
32
|
+
params[:body_md5] = Digest::MD5.hexdigest(body)
|
33
|
+
|
34
|
+
request = Authentication::Request.new('POST', @uri.path, params)
|
35
|
+
auth_hash = request.sign(Pusher.authentication_token)
|
36
|
+
|
37
|
+
query_params = params.merge(auth_hash)
|
38
|
+
@uri.query = query_params.to_params
|
39
|
+
|
40
|
+
response = @http.post("#{@uri.path}?#{@uri.query}", body, {
|
41
|
+
'Content-Type'=> 'application/json'
|
42
|
+
})
|
43
|
+
|
44
|
+
case response.code
|
45
|
+
when "202"
|
46
|
+
return true
|
47
|
+
when "401"
|
48
|
+
raise AuthenticationError, response.body.chomp
|
49
|
+
when "404"
|
50
|
+
raise Error, "Resource not found: app_id is probably invalid"
|
51
|
+
else
|
52
|
+
raise Error, "Unknown error in Pusher: #{response.body.chomp}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def trigger(event_name, data, socket_id = nil)
|
57
|
+
trigger!(event_name, data, socket_id)
|
58
|
+
rescue StandardError => e
|
59
|
+
handle_error e
|
22
60
|
end
|
23
61
|
|
24
62
|
def self.turn_into_json(data)
|
data/pusher.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{pusher}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.4.0.beta"
|
9
9
|
|
10
|
-
s.required_rubygems_version = Gem::Requirement.new("
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["New Bamboo"]
|
12
|
-
s.date = %q{2010-04-
|
12
|
+
s.date = %q{2010-04-22}
|
13
13
|
s.description = %q{Wrapper for pusherapp.com REST api}
|
14
14
|
s.email = %q{support@pusherapp.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -24,8 +24,10 @@ Gem::Specification.new do |s|
|
|
24
24
|
"Rakefile",
|
25
25
|
"VERSION",
|
26
26
|
"lib/pusher.rb",
|
27
|
+
"lib/pusher/authentication.rb",
|
27
28
|
"lib/pusher/channel.rb",
|
28
29
|
"pusher.gemspec",
|
30
|
+
"spec/authentication_spec.rb",
|
29
31
|
"spec/pusher_spec.rb",
|
30
32
|
"spec/spec.opts",
|
31
33
|
"spec/spec_helper.rb"
|
@@ -36,7 +38,8 @@ Gem::Specification.new do |s|
|
|
36
38
|
s.rubygems_version = %q{1.3.6}
|
37
39
|
s.summary = %q{Pusher App client}
|
38
40
|
s.test_files = [
|
39
|
-
"spec/
|
41
|
+
"spec/authentication_spec.rb",
|
42
|
+
"spec/pusher_spec.rb",
|
40
43
|
"spec/spec_helper.rb"
|
41
44
|
]
|
42
45
|
|
@@ -46,14 +49,20 @@ Gem::Specification.new do |s|
|
|
46
49
|
|
47
50
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
48
51
|
s.add_runtime_dependency(%q<json>, [">= 0"])
|
52
|
+
s.add_runtime_dependency(%q<crack>, [">= 0"])
|
49
53
|
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
54
|
+
s.add_development_dependency(%q<webmock>, [">= 0"])
|
50
55
|
else
|
51
56
|
s.add_dependency(%q<json>, [">= 0"])
|
57
|
+
s.add_dependency(%q<crack>, [">= 0"])
|
52
58
|
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
59
|
+
s.add_dependency(%q<webmock>, [">= 0"])
|
53
60
|
end
|
54
61
|
else
|
55
62
|
s.add_dependency(%q<json>, [">= 0"])
|
63
|
+
s.add_dependency(%q<crack>, [">= 0"])
|
56
64
|
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
65
|
+
s.add_dependency(%q<webmock>, [">= 0"])
|
57
66
|
end
|
58
67
|
end
|
59
68
|
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Authentication do
|
4
|
+
before :each do
|
5
|
+
Time.stub!(:now).and_return(Time.at(1234))
|
6
|
+
|
7
|
+
@token = Authentication::Token.new('key', 'secret')
|
8
|
+
|
9
|
+
@request = Authentication::Request.new('POST', '/some/path', {
|
10
|
+
"query" => "params",
|
11
|
+
"go" => "here"
|
12
|
+
})
|
13
|
+
@signature = @request.sign(@token)[:auth_signature]
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should generate base64 encoded signature from correct key" do
|
17
|
+
@request.send(:string_to_sign).should == "POST\n/some/path\nauth_key=key&auth_timestamp=1234&auth_version=1.0&go=here&query=params"
|
18
|
+
@signature.should == 'OyN5U6W6ZhmHXLsqLUPo2p71gk6KLGifYoSshbweoNs='
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should make auth_hash available after request is signed" do
|
22
|
+
request = Authentication::Request.new('POST', '/some/path', {
|
23
|
+
"query" => "params"
|
24
|
+
})
|
25
|
+
lambda {
|
26
|
+
request.auth_hash
|
27
|
+
}.should raise_error('Request not signed')
|
28
|
+
|
29
|
+
request.sign(@token)
|
30
|
+
request.auth_hash.should == {
|
31
|
+
:auth_signature => "2gePzt1ylBtshzyqQNDWsgAOv8cAzugCsSjdIPcudOk=",
|
32
|
+
:auth_version => "1.0",
|
33
|
+
:auth_key => "key",
|
34
|
+
:auth_timestamp => 1234
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should cope with symbol keys" do
|
39
|
+
@request.query_hash = {
|
40
|
+
:query => "params",
|
41
|
+
:go => "here"
|
42
|
+
}
|
43
|
+
@request.sign(@token)[:auth_signature].should == @signature
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should cope with upcase keys (keys are lowercased before signing)" do
|
47
|
+
@request.query_hash = {
|
48
|
+
"Query" => "params",
|
49
|
+
"GO" => "here"
|
50
|
+
}
|
51
|
+
@request.sign(@token)[:auth_signature].should == @signature
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should use the path to generate signature" do
|
55
|
+
@request.path = '/some/other/path'
|
56
|
+
@request.sign(@token)[:auth_signature].should_not == @signature
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should use the query string keys to generate signature" do
|
60
|
+
@request.query_hash = {
|
61
|
+
"other" => "query"
|
62
|
+
}
|
63
|
+
@request.sign(@token)[:auth_signature].should_not == @signature
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should use the query string values to generate signature" do
|
67
|
+
@request.query_hash = {
|
68
|
+
"key" => "notfoo",
|
69
|
+
"other" => 'bar'
|
70
|
+
}
|
71
|
+
@request.sign(@token)[:signature].should_not == @signature
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "verification" do
|
75
|
+
before :each do
|
76
|
+
Time.stub!(:now).and_return(Time.at(1234))
|
77
|
+
@request.sign(@token)
|
78
|
+
@params = @request.query_hash.merge(@request.auth_hash)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should verify requests" do
|
82
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
83
|
+
request.authenticate_by_token(@token).should == true
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should raise error if signature is not correct" do
|
87
|
+
@params[:auth_signature] = 'asdf'
|
88
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
89
|
+
lambda {
|
90
|
+
request.authenticate_by_token!(@token)
|
91
|
+
}.should raise_error('Invalid signature: you should have sent Base64Encode(HmacSHA256("POST\n/some/path\nauth_key=key&auth_timestamp=1234&auth_version=1.0&go=here&query=params", your_secret_key))')
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should raise error if timestamp not available" do
|
95
|
+
@params.delete(:auth_timestamp)
|
96
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
97
|
+
lambda {
|
98
|
+
request.authenticate_by_token!(@token)
|
99
|
+
}.should raise_error('Timestamp required')
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should raise error if timestamp has expired (default of 600s)" do
|
103
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
104
|
+
Time.stub!(:now).and_return(Time.at(1234 + 599))
|
105
|
+
request.authenticate_by_token!(@token).should == true
|
106
|
+
Time.stub!(:now).and_return(Time.at(1234 - 599))
|
107
|
+
request.authenticate_by_token!(@token).should == true
|
108
|
+
Time.stub!(:now).and_return(Time.at(1234 + 600))
|
109
|
+
lambda {
|
110
|
+
request.authenticate_by_token!(@token)
|
111
|
+
}.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:30:34Z)")
|
112
|
+
Time.stub!(:now).and_return(Time.at(1234 - 600))
|
113
|
+
lambda {
|
114
|
+
request.authenticate_by_token!(@token)
|
115
|
+
}.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:10:34Z)")
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should be possible to customize the timeout grace period" do
|
119
|
+
grace = 10
|
120
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
121
|
+
Time.stub!(:now).and_return(Time.at(1234 + grace - 1))
|
122
|
+
request.authenticate_by_token!(@token, grace).should == true
|
123
|
+
Time.stub!(:now).and_return(Time.at(1234 + grace))
|
124
|
+
lambda {
|
125
|
+
request.authenticate_by_token!(@token, grace)
|
126
|
+
}.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 10s of server time (1970-01-01T00:20:44Z)")
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should be possible to skip timestamp check by passing nil" do
|
130
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
131
|
+
Time.stub!(:now).and_return(Time.at(1234 + 1000))
|
132
|
+
request.authenticate_by_token!(@token, nil).should == true
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should check that auth_version is supplied" do
|
136
|
+
@params.delete(:auth_version)
|
137
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
138
|
+
lambda {
|
139
|
+
request.authenticate_by_token!(@token)
|
140
|
+
}.should raise_error('Version required')
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should check that auth_version equals 1.0" do
|
144
|
+
@params[:auth_version] = '1.1'
|
145
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
146
|
+
lambda {
|
147
|
+
request.authenticate_by_token!(@token)
|
148
|
+
}.should raise_error('Version not supported')
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "when used with optional block" do
|
152
|
+
it "should optionally take a block which yields the signature" do
|
153
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
154
|
+
request.authenticate do |key|
|
155
|
+
key.should == @token.key
|
156
|
+
@token
|
157
|
+
end.should == @token
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should raise error if no auth_key supplied to request" do
|
161
|
+
@params.delete(:auth_key)
|
162
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
163
|
+
lambda {
|
164
|
+
request.authenticate { |key| nil }
|
165
|
+
}.should raise_error('Authentication key required')
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should raise error if block returns nil (i.e. key doesn't exist)" do
|
169
|
+
request = Authentication::Request.new('POST', '/some/path', @params)
|
170
|
+
lambda {
|
171
|
+
request.authenticate { |key| nil }
|
172
|
+
}.should raise_error('Invalid authentication key')
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
data/spec/pusher_spec.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require File.expand_path('../spec_helper', __FILE__)
|
2
2
|
|
3
|
+
require 'webmock/rspec'
|
4
|
+
|
3
5
|
describe Pusher do
|
4
6
|
describe 'configuration' do
|
5
7
|
it 'should be preconfigured for api host' do
|
@@ -22,27 +24,17 @@ describe Pusher do
|
|
22
24
|
Pusher.logger.debug('foo')
|
23
25
|
Pusher.logger = nil
|
24
26
|
end
|
25
|
-
|
26
|
-
it 'should raise exception if key and secret are missing' do
|
27
|
-
lambda {
|
28
|
-
Pusher['test-channel']
|
29
|
-
}.should raise_error(Pusher::ArgumentError)
|
30
|
-
end
|
31
|
-
|
32
|
-
it 'should raise exception if key is missing' do
|
33
|
-
lambda {
|
34
|
-
Pusher['test-channel']
|
35
|
-
}.should raise_error(Pusher::ArgumentError)
|
36
|
-
end
|
37
27
|
end
|
38
28
|
|
39
29
|
describe 'configured' do
|
40
30
|
before do
|
31
|
+
Pusher.app_id = '20'
|
41
32
|
Pusher.key = '12345678900000001'
|
42
33
|
Pusher.secret = '12345678900000001'
|
43
34
|
end
|
44
35
|
|
45
36
|
after do
|
37
|
+
Pusher.app_id = nil
|
46
38
|
Pusher.key = nil
|
47
39
|
Pusher.secret = nil
|
48
40
|
end
|
@@ -55,34 +47,102 @@ describe Pusher do
|
|
55
47
|
it 'should return a new channel' do
|
56
48
|
@channel.should be_kind_of(Pusher::Channel)
|
57
49
|
end
|
50
|
+
|
51
|
+
%w{app_id key secret}.each do |config|
|
52
|
+
it "should raise exception if #{config} not configured" do
|
53
|
+
Pusher.send("#{config}=", nil)
|
54
|
+
lambda {
|
55
|
+
Pusher['test_channel']
|
56
|
+
}.should raise_error(ArgumentError)
|
57
|
+
end
|
58
|
+
end
|
58
59
|
end
|
59
60
|
|
60
|
-
describe 'Channel#trigger' do
|
61
|
-
before do
|
62
|
-
|
63
|
-
|
61
|
+
describe 'Channel#trigger!' do
|
62
|
+
before :each do
|
63
|
+
WebMock.stub_request(:post, %r{/app/20/channel/test_channel/event})
|
64
|
+
@channel = Pusher['test_channel']
|
64
65
|
end
|
65
66
|
|
66
67
|
it 'should configure HTTP library to talk to pusher API' do
|
67
|
-
|
68
|
-
|
69
|
-
Pusher['test_channel'].trigger('new_event', 'Some data')
|
68
|
+
@channel.trigger!('new_event', 'Some data')
|
69
|
+
WebMock.request(:post, %r{api.pusherapp.com}).should have_been_made
|
70
70
|
end
|
71
71
|
|
72
|
-
it 'should POST
|
73
|
-
@
|
74
|
-
path.should == '/app/12345678900000001/channel/test_channel'
|
75
|
-
parsed = JSON.parse(data)
|
76
|
-
parsed['event'].should == 'new_event'
|
77
|
-
parsed['data']['name'].should == 'Pusher'
|
78
|
-
parsed['data']['last_name'].should == 'App'
|
79
|
-
parsed['socket_id'].should == nil
|
80
|
-
headers.should == {'Content-Type'=> 'application/json'}
|
81
|
-
end
|
82
|
-
Pusher['test_channel'].trigger('new_event', {
|
72
|
+
it 'should POST with the correct parameters and convert data to JSON' do
|
73
|
+
@channel.trigger!('new_event', {
|
83
74
|
:name => 'Pusher',
|
84
75
|
:last_name => 'App'
|
85
76
|
})
|
77
|
+
WebMock.request(:post, %r{/app/20/channel/test_channel/event}).
|
78
|
+
with do |req|
|
79
|
+
|
80
|
+
query_hash = req.uri.query_values
|
81
|
+
query_hash["name"].should == 'new_event'
|
82
|
+
query_hash["auth_key"].should == Pusher.key
|
83
|
+
query_hash["auth_timestamp"].should_not be_nil
|
84
|
+
|
85
|
+
parsed = JSON.parse(req.body)
|
86
|
+
parsed.should == {
|
87
|
+
"name" => 'Pusher',
|
88
|
+
"last_name" => 'App'
|
89
|
+
}
|
90
|
+
|
91
|
+
req.headers['Content-Type'].should == 'application/json'
|
92
|
+
end.should have_been_made
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should handle string data by sending unmodified in body" do
|
96
|
+
string = "foo\nbar\""
|
97
|
+
@channel.trigger!('new_event', string)
|
98
|
+
WebMock.request(:post, %r{/app/20/channel/test_channel/event}).with do |req|
|
99
|
+
req.body.should == "foo\nbar\""
|
100
|
+
end.should have_been_made
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should raise error if an object sent which canot be JSONified" do
|
104
|
+
lambda {
|
105
|
+
@channel.trigger!('new_event', Object.new)
|
106
|
+
}.should raise_error(JSON::GeneratorError)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should propagate exception if exception raised" do
|
110
|
+
WebMock.stub_request(:post, %r{/app/20/channel/test_channel/event}).
|
111
|
+
to_raise(RuntimeError)
|
112
|
+
lambda {
|
113
|
+
Pusher['test_channel'].trigger!('new_event', 'Some data')
|
114
|
+
}.should raise_error(RuntimeError)
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should raise AuthenticationError if pusher returns 401" do
|
118
|
+
WebMock.stub_request(:post, %r{/app/20/channel/test_channel/event}).
|
119
|
+
to_return(:status => 401)
|
120
|
+
lambda {
|
121
|
+
Pusher['test_channel'].trigger!('new_event', 'Some data')
|
122
|
+
}.should raise_error(Pusher::AuthenticationError)
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should raise Pusher::Error if pusher returns 404" do
|
126
|
+
WebMock.stub_request(:post, %r{/app/20/channel/test_channel/event}).
|
127
|
+
to_return(:status => 404)
|
128
|
+
lambda {
|
129
|
+
Pusher['test_channel'].trigger!('new_event', 'Some data')
|
130
|
+
}.should raise_error(Pusher::Error, 'Resource not found: app_id is probably invalid')
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should raise Pusher::Error if pusher returns 500" do
|
134
|
+
WebMock.stub_request(:post, %r{/app/20/channel/test_channel/event}).
|
135
|
+
to_return(:status => 500, :body => "some error")
|
136
|
+
lambda {
|
137
|
+
Pusher['test_channel'].trigger!('new_event', 'Some data')
|
138
|
+
}.should raise_error(Pusher::Error, 'Unknown error in Pusher: some error')
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe 'Channel#trigger' do
|
143
|
+
before :each do
|
144
|
+
@http = mock('HTTP', :post => 'posting')
|
145
|
+
Net::HTTP.stub!(:new).and_return @http
|
86
146
|
end
|
87
147
|
|
88
148
|
it "should log failure if exception raised" do
|
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pusher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
4
|
+
prerelease: true
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
|
7
|
+
- 4
|
8
|
+
- 0
|
9
|
+
- beta
|
10
|
+
version: 0.4.0.beta
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- New Bamboo
|
@@ -14,7 +15,7 @@ autorequire:
|
|
14
15
|
bindir: bin
|
15
16
|
cert_chain: []
|
16
17
|
|
17
|
-
date: 2010-04-
|
18
|
+
date: 2010-04-22 00:00:00 +01:00
|
18
19
|
default_executable:
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
@@ -30,9 +31,21 @@ dependencies:
|
|
30
31
|
type: :runtime
|
31
32
|
version_requirements: *id001
|
32
33
|
- !ruby/object:Gem::Dependency
|
33
|
-
name:
|
34
|
+
name: crack
|
34
35
|
prerelease: false
|
35
36
|
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
segments:
|
41
|
+
- 0
|
42
|
+
version: "0"
|
43
|
+
type: :runtime
|
44
|
+
version_requirements: *id002
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: rspec
|
47
|
+
prerelease: false
|
48
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
36
49
|
requirements:
|
37
50
|
- - ">="
|
38
51
|
- !ruby/object:Gem::Version
|
@@ -42,7 +55,19 @@ dependencies:
|
|
42
55
|
- 9
|
43
56
|
version: 1.2.9
|
44
57
|
type: :development
|
45
|
-
version_requirements: *
|
58
|
+
version_requirements: *id003
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: webmock
|
61
|
+
prerelease: false
|
62
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
type: :development
|
70
|
+
version_requirements: *id004
|
46
71
|
description: Wrapper for pusherapp.com REST api
|
47
72
|
email: support@pusherapp.com
|
48
73
|
executables: []
|
@@ -60,8 +85,10 @@ files:
|
|
60
85
|
- Rakefile
|
61
86
|
- VERSION
|
62
87
|
- lib/pusher.rb
|
88
|
+
- lib/pusher/authentication.rb
|
63
89
|
- lib/pusher/channel.rb
|
64
90
|
- pusher.gemspec
|
91
|
+
- spec/authentication_spec.rb
|
65
92
|
- spec/pusher_spec.rb
|
66
93
|
- spec/spec.opts
|
67
94
|
- spec/spec_helper.rb
|
@@ -83,11 +110,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
110
|
version: "0"
|
84
111
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
112
|
requirements:
|
86
|
-
- - "
|
113
|
+
- - ">"
|
87
114
|
- !ruby/object:Gem::Version
|
88
115
|
segments:
|
89
|
-
-
|
90
|
-
|
116
|
+
- 1
|
117
|
+
- 3
|
118
|
+
- 1
|
119
|
+
version: 1.3.1
|
91
120
|
requirements: []
|
92
121
|
|
93
122
|
rubyforge_project:
|
@@ -96,5 +125,6 @@ signing_key:
|
|
96
125
|
specification_version: 3
|
97
126
|
summary: Pusher App client
|
98
127
|
test_files:
|
128
|
+
- spec/authentication_spec.rb
|
99
129
|
- spec/pusher_spec.rb
|
100
130
|
- spec/spec_helper.rb
|