pusher 0.5.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -4,3 +4,8 @@
4
4
 
5
5
  * Use ActiveSupport::JSON.encode instead of #to_json to avoid conflicts with JSON gem. [OL]
6
6
  * Support loading Pusher credentials from a PUSHER_URL environment variable. [ML]
7
+
8
+ * Pusher v0.5.2 (25th May, 2010) *
9
+
10
+ * Bugfix: required uri correctly
11
+ * Bugfix: channel objects not being reused
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.1
1
+ 0.5.2
data/lib/pusher.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  autoload 'Logger', 'logger'
2
+ require 'uri'
2
3
 
3
4
  module Pusher
4
5
  class Error < RuntimeError; end
5
6
  class AuthenticationError < Error; end
7
+ class ConfigurationError < Error; end
6
8
 
7
9
  class << self
8
10
  attr_accessor :host, :port
@@ -55,9 +57,9 @@ module Pusher
55
57
  end
56
58
 
57
59
  def self.[](channel_name)
58
- raise ArgumentError, 'Missing configuration: please check that Pusher.url is configured' unless configured?
60
+ raise ConfigurationError, 'Missing configuration: please check that Pusher.url is configured' unless configured?
59
61
  @channels ||= {}
60
- @channels[channel_name.to_s] = Channel.new(url, channel_name)
62
+ @channels[channel_name.to_s] ||= Channel.new(url, channel_name)
61
63
  end
62
64
  end
63
65
 
@@ -1,11 +1,6 @@
1
1
  require 'crack/core_extensions' # Used for Hash#to_params
2
- require 'signature'
3
- require 'digest/md5'
4
2
  require 'hmac-sha2'
5
3
 
6
- require 'json'
7
- require 'uri'
8
-
9
4
  module Pusher
10
5
  class Channel
11
6
  attr_reader :name
@@ -1,3 +1,7 @@
1
+ require 'signature'
2
+ require 'digest/md5'
3
+ require 'json'
4
+
1
5
  module Pusher
2
6
  class Request
3
7
  attr_reader :body, :query
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.5.1"
8
+ s.version = "0.5.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["New Bamboo"]
12
- s.date = %q{2010-05-20}
12
+ s.date = %q{2010-05-25}
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 = [
@@ -28,6 +28,7 @@ Gem::Specification.new do |s|
28
28
  "lib/pusher/channel.rb",
29
29
  "lib/pusher/request.rb",
30
30
  "pusher.gemspec",
31
+ "spec/channel_spec.rb",
31
32
  "spec/pusher_spec.rb",
32
33
  "spec/spec.opts",
33
34
  "spec/spec_helper.rb"
@@ -38,7 +39,8 @@ Gem::Specification.new do |s|
38
39
  s.rubygems_version = %q{1.3.6}
39
40
  s.summary = %q{Pusher App client}
40
41
  s.test_files = [
41
- "spec/pusher_spec.rb",
42
+ "spec/channel_spec.rb",
43
+ "spec/pusher_spec.rb",
42
44
  "spec/spec_helper.rb"
43
45
  ]
44
46
 
@@ -0,0 +1,239 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Pusher::Channel do
4
+ before do
5
+ Pusher.app_id = '20'
6
+ Pusher.key = '12345678900000001'
7
+ Pusher.secret = '12345678900000001'
8
+ end
9
+
10
+ after do
11
+ Pusher.app_id = nil
12
+ Pusher.key = nil
13
+ Pusher.secret = nil
14
+ end
15
+
16
+ describe 'trigger!' do
17
+ before :each do
18
+ WebMock.stub_request(
19
+ :post, %r{/apps/20/channels/test_channel/events}
20
+ ).to_return(:status => 202)
21
+ @channel = Pusher['test_channel']
22
+ end
23
+
24
+ it 'should configure HTTP library to talk to pusher API' do
25
+ @channel.trigger!('new_event', 'Some data')
26
+ WebMock.request(:post, %r{api.pusherapp.com}).should have_been_made
27
+ end
28
+
29
+ it 'should POST with the correct parameters and convert data to JSON' do
30
+ @channel.trigger!('new_event', {
31
+ :name => 'Pusher',
32
+ :last_name => 'App'
33
+ })
34
+ WebMock.request(:post, %r{/apps/20/channels/test_channel/events}).
35
+ with do |req|
36
+
37
+ query_hash = req.uri.query_values
38
+ query_hash["name"].should == 'new_event'
39
+ query_hash["auth_key"].should == Pusher.key
40
+ query_hash["auth_timestamp"].should_not be_nil
41
+
42
+ parsed = JSON.parse(req.body)
43
+ parsed.should == {
44
+ "name" => 'Pusher',
45
+ "last_name" => 'App'
46
+ }
47
+
48
+ req.headers['Content-Type'].should == 'application/json'
49
+ end.should have_been_made
50
+ end
51
+
52
+ it "should handle string data by sending unmodified in body" do
53
+ string = "foo\nbar\""
54
+ @channel.trigger!('new_event', string)
55
+ WebMock.request(:post, %r{/apps/20/channels/test_channel/events}).with do |req|
56
+ req.body.should == "foo\nbar\""
57
+ end.should have_been_made
58
+ end
59
+
60
+ it "should raise error if an object sent which canot be JSONified" do
61
+ lambda {
62
+ @channel.trigger!('new_event', Object.new)
63
+ }.should raise_error(JSON::GeneratorError)
64
+ end
65
+
66
+ it "should propagate exception if exception raised" do
67
+ WebMock.stub_request(
68
+ :post, %r{/apps/20/channels/test_channel/events}
69
+ ).to_raise(RuntimeError)
70
+ lambda {
71
+ Pusher['test_channel'].trigger!('new_event', 'Some data')
72
+ }.should raise_error(RuntimeError)
73
+ end
74
+
75
+ it "should raise AuthenticationError if pusher returns 401" do
76
+ WebMock.stub_request(
77
+ :post,
78
+ %r{/apps/20/channels/test_channel/events}
79
+ ).to_return(:status => 401)
80
+ lambda {
81
+ Pusher['test_channel'].trigger!('new_event', 'Some data')
82
+ }.should raise_error(Pusher::AuthenticationError)
83
+ end
84
+
85
+ it "should raise Pusher::Error if pusher returns 404" do
86
+ WebMock.stub_request(
87
+ :post, %r{/apps/20/channels/test_channel/events}
88
+ ).to_return(:status => 404)
89
+ lambda {
90
+ Pusher['test_channel'].trigger!('new_event', 'Some data')
91
+ }.should raise_error(Pusher::Error, 'Resource not found: app_id is probably invalid')
92
+ end
93
+
94
+ it "should raise Pusher::Error if pusher returns 500" do
95
+ WebMock.stub_request(
96
+ :post, %r{/apps/20/channels/test_channel/events}
97
+ ).to_return(:status => 500, :body => "some error")
98
+ lambda {
99
+ Pusher['test_channel'].trigger!('new_event', 'Some data')
100
+ }.should raise_error(Pusher::Error, 'Unknown error in Pusher: some error')
101
+ end
102
+ end
103
+
104
+ describe 'trigger' do
105
+ before :each do
106
+ @http = mock('HTTP', :post => 'posting')
107
+ Net::HTTP.stub!(:new).and_return @http
108
+ end
109
+
110
+ it "should log failure if exception raised" do
111
+ @http.should_receive(:post).and_raise("Fail")
112
+ Pusher.logger.should_receive(:error).with("Fail (RuntimeError)")
113
+ Pusher.logger.should_receive(:debug) #backtrace
114
+ Pusher::Channel.new(Pusher.url, 'test_channel').trigger('new_event', 'Some data')
115
+ end
116
+
117
+ it "should log failure if exception raised" do
118
+ @http.should_receive(:post).and_raise("Fail")
119
+ Pusher.logger.should_receive(:error).with("Fail (RuntimeError)")
120
+ Pusher.logger.should_receive(:debug) #backtrace
121
+ Pusher::Channel.new(Pusher.url, 'test_channel').trigger('new_event', 'Some data')
122
+ end
123
+ end
124
+
125
+ describe "trigger_async" do
126
+ before :each do
127
+ EM.send(:remove_const, :HttpRequest)
128
+ EM::HttpRequest = EM::MockHttpRequest
129
+
130
+ #in order to match URLs when testing http requests
131
+ #override the method that converts query hash to string
132
+ #to include a sort so URL is consistent
133
+ module EventMachine
134
+ module HttpEncoding
135
+ def encode_query(path, query, uri_query)
136
+ encoded_query = if query.kind_of?(Hash)
137
+ query.sort{|a, b| a.to_s <=> b.to_s}.
138
+ map { |k, v| encode_param(k, v) }.
139
+ join('&')
140
+ else
141
+ query.to_s
142
+ end
143
+ if !uri_query.to_s.empty?
144
+ encoded_query = [encoded_query, uri_query].reject {|part| part.empty?}.join("&")
145
+ end
146
+ return path if encoded_query.to_s.empty?
147
+ "#{path}?#{encoded_query}"
148
+ end
149
+ end
150
+ end
151
+
152
+ EM::HttpRequest.reset_registry!
153
+ EM::HttpRequest.reset_counts!
154
+ EM::HttpRequest.pass_through_requests = false
155
+ end
156
+
157
+ it "should return a deferrable which succeeds in success case" do
158
+ # Yeah, mocking EM::MockHttpRequest isn't that feature rich :)
159
+ Time.stub(:now).and_return(123)
160
+
161
+ url = 'http://api.pusherapp.com:80/apps/20/channels/test_channel/events?auth_key=12345678900000001&auth_signature=0ffe2a3749f886ca69c3f516a30c7bc9a12d2ebd8bda5b718b90ad58507c8261&auth_timestamp=123&auth_version=1.0&body_md5=5b82f8bf4df2bfb0e66ccaa7306fd024&name=new_event'
162
+
163
+ data = <<-RESPONSE.gsub(/^ +/, '')
164
+ HTTP/1.1 202 Accepted
165
+ Content-Type: text/html
166
+ Content-Length: 13
167
+ Connection: keep-alive
168
+ Server: thin 1.2.7 codename No Hup
169
+
170
+ 202 ACCEPTED
171
+ RESPONSE
172
+
173
+ EM::HttpRequest.register(url, :post, data)
174
+
175
+ EM.run {
176
+ d = Pusher['test_channel'].trigger_async('new_event', 'Some data')
177
+ d.callback {
178
+ EM::HttpRequest.count(url, :post).should == 1
179
+ EM.stop
180
+ }
181
+ d.errback {
182
+ fail
183
+ EM.stop
184
+ }
185
+ }
186
+ end
187
+
188
+ it "should return a deferrable which fails (with exception) in fail case" do
189
+ # Yeah, mocking EM::MockHttpRequest isn't that feature rich :)
190
+ Time.stub(:now).and_return(123)
191
+
192
+ url = 'http://api.pusherapp.com:80/apps/20/channels/test_channel/events?auth_key=12345678900000001&auth_signature=0ffe2a3749f886ca69c3f516a30c7bc9a12d2ebd8bda5b718b90ad58507c8261&auth_timestamp=123&auth_version=1.0&body_md5=5b82f8bf4df2bfb0e66ccaa7306fd024&name=new_event'
193
+
194
+ data = <<-RESPONSE.gsub(/^ +/, '')
195
+ HTTP/1.1 401 Unauthorized
196
+ Content-Type: text/html
197
+ Content-Length: 130
198
+ Connection: keep-alive
199
+ Server: thin 1.2.7 codename No Hup
200
+
201
+ 401 UNAUTHORIZED: Timestamp expired: Given timestamp (2010-05-05T11:24:42Z) not within 600s of server time (2010-05-05T11:51:42Z)
202
+ RESPONSE
203
+
204
+ EM::HttpRequest.register(url, :post, data)
205
+
206
+ EM.run {
207
+ d = Pusher['test_channel'].trigger_async('new_event', 'Some data')
208
+ d.callback {
209
+ fail
210
+ }
211
+ d.errback { |error|
212
+ EM::HttpRequest.count(url, :post).should == 1
213
+ error.should be_kind_of(Pusher::AuthenticationError)
214
+ EM.stop
215
+ }
216
+ }
217
+ end
218
+ end
219
+
220
+ describe "socket_auth" do
221
+ before :each do
222
+ @channel = Pusher['test_channel']
223
+ end
224
+
225
+ it "should return an authentication string given a socket id" do
226
+ auth = @channel.socket_auth('socketid')
227
+
228
+ auth.should == '12345678900000001:827076f551e22451357939e4c7bb1200de29f921d5bf80b40d71668f9cd61c40'
229
+ end
230
+
231
+ it "should raise error if authentication is invalid" do
232
+ [nil, ''].each do |invalid|
233
+ lambda {
234
+ @channel.socket_auth(invalid)
235
+ }.should raise_error
236
+ end
237
+ end
238
+ end
239
+ end
data/spec/pusher_spec.rb CHANGED
@@ -48,7 +48,7 @@ describe Pusher do
48
48
  end
49
49
  end
50
50
 
51
- describe 'configured' do
51
+ describe 'when configured' do
52
52
  before do
53
53
  Pusher.app_id = '20'
54
54
  Pusher.key = '12345678900000001'
@@ -66,238 +66,22 @@ describe Pusher do
66
66
  @channel = Pusher['test_channel']
67
67
  end
68
68
 
69
- it 'should return a new channel' do
69
+ it 'should return a channel' do
70
70
  @channel.should be_kind_of(Pusher::Channel)
71
71
  end
72
72
 
73
+ it "should reuse the same channel objects" do
74
+ channel1, channel2 = Pusher['test_channel'], Pusher['test_channel']
75
+
76
+ channel1.object_id.should == channel2.object_id
77
+ end
78
+
73
79
  %w{app_id key secret}.each do |config|
74
80
  it "should raise exception if #{config} not configured" do
75
81
  Pusher.send("#{config}=", nil)
76
82
  lambda {
77
83
  Pusher['test_channel']
78
- }.should raise_error(ArgumentError)
79
- end
80
- end
81
- end
82
-
83
- describe 'Channel#trigger!' do
84
- before :each do
85
- WebMock.stub_request(
86
- :post, %r{/apps/20/channels/test_channel/events}
87
- ).to_return(:status => 202)
88
- @channel = Pusher['test_channel']
89
- end
90
-
91
- it 'should configure HTTP library to talk to pusher API' do
92
- @channel.trigger!('new_event', 'Some data')
93
- WebMock.request(:post, %r{api.pusherapp.com}).should have_been_made
94
- end
95
-
96
- it 'should POST with the correct parameters and convert data to JSON' do
97
- @channel.trigger!('new_event', {
98
- :name => 'Pusher',
99
- :last_name => 'App'
100
- })
101
- WebMock.request(:post, %r{/apps/20/channels/test_channel/events}).
102
- with do |req|
103
-
104
- query_hash = req.uri.query_values
105
- query_hash["name"].should == 'new_event'
106
- query_hash["auth_key"].should == Pusher.key
107
- query_hash["auth_timestamp"].should_not be_nil
108
-
109
- parsed = JSON.parse(req.body)
110
- parsed.should == {
111
- "name" => 'Pusher',
112
- "last_name" => 'App'
113
- }
114
-
115
- req.headers['Content-Type'].should == 'application/json'
116
- end.should have_been_made
117
- end
118
-
119
- it "should handle string data by sending unmodified in body" do
120
- string = "foo\nbar\""
121
- @channel.trigger!('new_event', string)
122
- WebMock.request(:post, %r{/apps/20/channels/test_channel/events}).with do |req|
123
- req.body.should == "foo\nbar\""
124
- end.should have_been_made
125
- end
126
-
127
- it "should raise error if an object sent which canot be JSONified" do
128
- lambda {
129
- @channel.trigger!('new_event', Object.new)
130
- }.should raise_error(JSON::GeneratorError)
131
- end
132
-
133
- it "should propagate exception if exception raised" do
134
- WebMock.stub_request(
135
- :post, %r{/apps/20/channels/test_channel/events}
136
- ).to_raise(RuntimeError)
137
- lambda {
138
- Pusher['test_channel'].trigger!('new_event', 'Some data')
139
- }.should raise_error(RuntimeError)
140
- end
141
-
142
- it "should raise AuthenticationError if pusher returns 401" do
143
- WebMock.stub_request(
144
- :post,
145
- %r{/apps/20/channels/test_channel/events}
146
- ).to_return(:status => 401)
147
- lambda {
148
- Pusher['test_channel'].trigger!('new_event', 'Some data')
149
- }.should raise_error(Pusher::AuthenticationError)
150
- end
151
-
152
- it "should raise Pusher::Error if pusher returns 404" do
153
- WebMock.stub_request(
154
- :post, %r{/apps/20/channels/test_channel/events}
155
- ).to_return(:status => 404)
156
- lambda {
157
- Pusher['test_channel'].trigger!('new_event', 'Some data')
158
- }.should raise_error(Pusher::Error, 'Resource not found: app_id is probably invalid')
159
- end
160
-
161
- it "should raise Pusher::Error if pusher returns 500" do
162
- WebMock.stub_request(
163
- :post, %r{/apps/20/channels/test_channel/events}
164
- ).to_return(:status => 500, :body => "some error")
165
- lambda {
166
- Pusher['test_channel'].trigger!('new_event', 'Some data')
167
- }.should raise_error(Pusher::Error, 'Unknown error in Pusher: some error')
168
- end
169
- end
170
-
171
- describe 'Channel#trigger' do
172
- before :each do
173
- @http = mock('HTTP', :post => 'posting')
174
- Net::HTTP.stub!(:new).and_return @http
175
- end
176
-
177
- it "should log failure if exception raised" do
178
- @http.should_receive(:post).and_raise("Fail")
179
- Pusher.logger.should_receive(:error).with("Fail (RuntimeError)")
180
- Pusher.logger.should_receive(:debug) #backtrace
181
- Pusher['test_channel'].trigger('new_event', 'Some data')
182
- end
183
-
184
- it "should log failure if exception raised" do
185
- @http.should_receive(:post).and_raise("Fail")
186
- Pusher.logger.should_receive(:error).with("Fail (RuntimeError)")
187
- Pusher.logger.should_receive(:debug) #backtrace
188
- Pusher['test_channel'].trigger('new_event', 'Some data')
189
- end
190
- end
191
-
192
- describe "Channel#trigger_async" do
193
- #in order to match URLs when testing http requests
194
- #override the method that converts query hash to string
195
- #to include a sort so URL is consistent
196
- module EventMachine
197
- module HttpEncoding
198
- def encode_query(path, query, uri_query)
199
- encoded_query = if query.kind_of?(Hash)
200
- query.sort{|a, b| a.to_s <=> b.to_s}.
201
- map { |k, v| encode_param(k, v) }.
202
- join('&')
203
- else
204
- query.to_s
205
- end
206
- if !uri_query.to_s.empty?
207
- encoded_query = [encoded_query, uri_query].reject {|part| part.empty?}.join("&")
208
- end
209
- return path if encoded_query.to_s.empty?
210
- "#{path}?#{encoded_query}"
211
- end
212
- end
213
- end
214
-
215
- before :each do
216
- EM::HttpRequest = EM::MockHttpRequest
217
- EM::HttpRequest.reset_registry!
218
- EM::HttpRequest.reset_counts!
219
- EM::HttpRequest.pass_through_requests = false
220
- end
221
-
222
- it "should return a deferrable which succeeds in success case" do
223
- # Yeah, mocking EM::MockHttpRequest isn't that feature rich :)
224
- Time.stub(:now).and_return(123)
225
-
226
- url = 'http://api.pusherapp.com:80/apps/20/channels/test_channel/events?auth_key=12345678900000001&auth_signature=0ffe2a3749f886ca69c3f516a30c7bc9a12d2ebd8bda5b718b90ad58507c8261&auth_timestamp=123&auth_version=1.0&body_md5=5b82f8bf4df2bfb0e66ccaa7306fd024&name=new_event'
227
-
228
- data = <<-RESPONSE.gsub(/^ +/, '')
229
- HTTP/1.1 202 Accepted
230
- Content-Type: text/html
231
- Content-Length: 13
232
- Connection: keep-alive
233
- Server: thin 1.2.7 codename No Hup
234
-
235
- 202 ACCEPTED
236
- RESPONSE
237
-
238
- EM::HttpRequest.register(url, :post, data)
239
-
240
- EM.run {
241
- d = Pusher['test_channel'].trigger_async('new_event', 'Some data')
242
- d.callback {
243
- EM::HttpRequest.count(url, :post).should == 1
244
- EM.stop
245
- }
246
- d.errback {
247
- fail
248
- EM.stop
249
- }
250
- }
251
- end
252
-
253
- it "should return a deferrable which fails (with exception) in fail case" do
254
- # Yeah, mocking EM::MockHttpRequest isn't that feature rich :)
255
- Time.stub(:now).and_return(123)
256
-
257
- url = 'http://api.pusherapp.com:80/apps/20/channels/test_channel/events?auth_key=12345678900000001&auth_signature=0ffe2a3749f886ca69c3f516a30c7bc9a12d2ebd8bda5b718b90ad58507c8261&auth_timestamp=123&auth_version=1.0&body_md5=5b82f8bf4df2bfb0e66ccaa7306fd024&name=new_event'
258
-
259
- data = <<-RESPONSE.gsub(/^ +/, '')
260
- HTTP/1.1 401 Unauthorized
261
- Content-Type: text/html
262
- Content-Length: 130
263
- Connection: keep-alive
264
- Server: thin 1.2.7 codename No Hup
265
-
266
- 401 UNAUTHORIZED: Timestamp expired: Given timestamp (2010-05-05T11:24:42Z) not within 600s of server time (2010-05-05T11:51:42Z)
267
- RESPONSE
268
-
269
- EM::HttpRequest.register(url, :post, data)
270
-
271
- EM.run {
272
- d = Pusher['test_channel'].trigger_async('new_event', 'Some data')
273
- d.callback {
274
- fail
275
- }
276
- d.errback { |error|
277
- EM::HttpRequest.count(url, :post).should == 1
278
- error.should be_kind_of(Pusher::AuthenticationError)
279
- EM.stop
280
- }
281
- }
282
- end
283
- end
284
-
285
- describe "Channel#socket_auth" do
286
- before :each do
287
- @channel = Pusher['test_channel']
288
- end
289
-
290
- it "should return an authentication string given a socket id" do
291
- auth = @channel.socket_auth('socketid')
292
-
293
- auth.should == '12345678900000001:827076f551e22451357939e4c7bb1200de29f921d5bf80b40d71668f9cd61c40'
294
- end
295
-
296
- it "should raise error if authentication is invalid" do
297
- [nil, ''].each do |invalid|
298
- lambda {
299
- @channel.socket_auth(invalid)
300
- }.should raise_error
84
+ }.should raise_error(Pusher::ConfigurationError)
301
85
  end
302
86
  end
303
87
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 5
8
- - 1
9
- version: 0.5.1
8
+ - 2
9
+ version: 0.5.2
10
10
  platform: ruby
11
11
  authors:
12
12
  - New Bamboo
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-05-20 00:00:00 +01:00
17
+ date: 2010-05-25 00:00:00 +01:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -112,6 +112,7 @@ files:
112
112
  - lib/pusher/channel.rb
113
113
  - lib/pusher/request.rb
114
114
  - pusher.gemspec
115
+ - spec/channel_spec.rb
115
116
  - spec/pusher_spec.rb
116
117
  - spec/spec.opts
117
118
  - spec/spec_helper.rb
@@ -146,5 +147,6 @@ signing_key:
146
147
  specification_version: 3
147
148
  summary: Pusher App client
148
149
  test_files:
150
+ - spec/channel_spec.rb
149
151
  - spec/pusher_spec.rb
150
152
  - spec/spec_helper.rb