tweetwine 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ require "rest_client"
2
+
3
+ module Tweetwine
4
+ class ClientError < RuntimeError; end
5
+
6
+ class RestClientWrapper
7
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ }
8
+
9
+ MAX_RETRIES = 3
10
+ RETRY_BASE_WAIT_TIMEOUT = 4
11
+
12
+ def initialize(io)
13
+ @io = io
14
+ end
15
+
16
+ protected
17
+
18
+ def method_missing(name, *args, &block)
19
+ tries = 0
20
+ begin
21
+ tries += 1
22
+ RestClient.send(name, *args, &block)
23
+ rescue Errno::ECONNRESET => e
24
+ if tries < MAX_RETRIES
25
+ timeout = RETRY_BASE_WAIT_TIMEOUT**tries
26
+ @io.warn("Could not connect -- retrying in #{timeout} seconds")
27
+ sleep timeout
28
+ retry
29
+ else
30
+ raise ClientError, e
31
+ end
32
+ rescue RestClient::Exception, SocketError, SystemCallError => e
33
+ raise ClientError, e
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ require "yaml"
2
+
3
+ module Tweetwine
4
+ class StartupConfig
5
+ attr_reader :options, :command, :args, :supported_commands
6
+
7
+ def initialize(supported_commands)
8
+ @supported_commands = supported_commands.to_a
9
+ raise ArgumentError, "Must give at least one supported command" if @supported_commands.empty?
10
+ @options = {}
11
+ @command = @supported_commands.first
12
+ @args = []
13
+ end
14
+
15
+ def parse(args = [], config_file = nil, &cmd_parser)
16
+ options = parse_options(args, config_file, &cmd_parser)
17
+ command = if args.empty? then @supported_commands.first else args.shift.to_sym end
18
+ raise ArgumentError, "Unknown command" unless @supported_commands.include? command
19
+ @options, @command, @args = options, command, args
20
+ self
21
+ end
22
+
23
+ private
24
+
25
+ def parse_options(args, config_file, &cmd_parser)
26
+ cmd_options = if cmd_parser then parse_cmdline_args(args, &cmd_parser) else {} end
27
+ config_options = if config_file && File.exists?(config_file) then parse_config_file(config_file) else {} end
28
+ config_options.merge(cmd_options)
29
+ end
30
+
31
+ def parse_cmdline_args(args, &cmd_parser)
32
+ cmd_parser.call(args)
33
+ end
34
+
35
+ def parse_config_file(config_file)
36
+ options = YAML.load(File.read(config_file))
37
+ Util.symbolize_hash_keys(options)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ module Tweetwine
2
+ class UrlShortener
3
+ def initialize(rest_client, options)
4
+ @rest_client = rest_client
5
+ options = Options.new(options, "URL shortening")
6
+ @method = (options[:method] || :get).to_sym
7
+ @service_url = options.require :service_url
8
+ @url_param_name = options.require :url_param_name
9
+ @extra_params = options[:extra_params] || {}
10
+ if @method == :get
11
+ tmp = []
12
+ @extra_params.each_pair { |k, v| tmp << "#{k}=#{v}" }
13
+ @extra_params = tmp
14
+ end
15
+ @xpath_selector = options.require :xpath_selector
16
+ end
17
+
18
+ def shorten(url)
19
+ require "nokogiri"
20
+ rest = case @method
21
+ when :get
22
+ tmp = @extra_params.dup
23
+ tmp << "#{@url_param_name}=#{url}"
24
+ service_url = "#{@service_url}?#{tmp.join('&')}"
25
+ [service_url]
26
+ when :post
27
+ service_url = @service_url
28
+ params = @extra_params.merge({ @url_param_name.to_sym => url })
29
+ [service_url, params]
30
+ else
31
+ raise "Unrecognized HTTP request method"
32
+ end
33
+ response = @rest_client.send(@method, *rest)
34
+ doc = Nokogiri::HTML(response)
35
+ doc.xpath(@xpath_selector).first.to_s
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ require "time"
2
+
3
+ module Tweetwine
4
+ module Util
5
+ def self.humanize_time_diff(from, to)
6
+ from = Time.parse(from.to_s) unless from.is_a? Time
7
+ to = Time.parse(to.to_s) unless to.is_a? Time
8
+
9
+ difference = (to - from).to_i.abs
10
+
11
+ value, unit = case difference
12
+ when 0..59 then [difference, "sec"]
13
+ when 60..3599 then [(difference/60.0).round, "min"]
14
+ when 3600..86399 then [(difference/3600.0).round, "hour"]
15
+ else [(difference/86400.0).round, "day"]
16
+ end
17
+
18
+ [value, pluralize_unit(value, unit)]
19
+ end
20
+
21
+ def self.symbolize_hash_keys(hash)
22
+ hash.inject({}) do |result, pair|
23
+ value = pair.last
24
+ value = symbolize_hash_keys(value) if value.is_a? Hash
25
+ result[pair.first.to_sym] = value
26
+ result
27
+ end
28
+ end
29
+
30
+ def self.parse_int_gt(value, default, min, name_for_error)
31
+ if value
32
+ value = value.to_i
33
+ if value >= min
34
+ value
35
+ else
36
+ raise ArgumentError, "Invalid #{name_for_error} -- must be greater than or equal to #{min}"
37
+ end
38
+ else
39
+ default
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def self.pluralize_unit(value, unit)
46
+ if ["hour", "day"].include?(unit) && value > 1
47
+ unit = unit + "s"
48
+ end
49
+ unit
50
+ end
51
+ end
52
+ end
data/lib/tweetwine.rb ADDED
@@ -0,0 +1,12 @@
1
+ %w{
2
+ meta
3
+ util
4
+ options
5
+ startup_config
6
+ io
7
+ rest_client_wrapper
8
+ url_shortener
9
+ client
10
+ }.each do |f|
11
+ require "tweetwine/#{f}"
12
+ end
@@ -0,0 +1,475 @@
1
+ require "test_helper"
2
+ require "json"
3
+
4
+ Mocha::Configuration.allow(:stubbing_non_existent_method)
5
+
6
+ module Tweetwine
7
+
8
+ class ClientTest < Test::Unit::TestCase
9
+ context "A client instance" do
10
+ setup do
11
+ @io = mock()
12
+ @rest_client = mock()
13
+ @url_shortener = mock()
14
+ url_shortener = lambda { |options| @url_shortener }
15
+ @deps = {
16
+ :io => @io,
17
+ :rest_client => @rest_client,
18
+ :url_shortener => url_shortener
19
+ }
20
+ end
21
+
22
+ context "upon initialization" do
23
+ should "raise exception when no authentication data is given" do
24
+ assert_raise(ArgumentError) { Client.new(@deps, {}) }
25
+ assert_raise(ArgumentError) { Client.new(@deps, { :password => "bar" }) }
26
+ assert_raise(ArgumentError) { Client.new(@deps, { :username => "", :password => "bar" }) }
27
+ assert_nothing_raised { Client.new(@deps, { :username => "foo", :password => "bar" }) }
28
+ end
29
+
30
+ should "use default number of statuses if not configured otherwise" do
31
+ @client = Client.new(@deps, { :username => "foo", :password => "bar" })
32
+ assert_equal Client::DEFAULT_NUM_STATUSES, @client.num_statuses
33
+ end
34
+
35
+ should "use configured number of statuses if in allowed range" do
36
+ @client = Client.new(@deps, { :username => "foo", :password => "bar", :num_statuses => 12 })
37
+ assert_equal 12, @client.num_statuses
38
+ end
39
+
40
+ should "raise an exception for configured number of statuses if not in allowed range" do
41
+ assert_raise(ArgumentError) { Client.new(@deps, { :username => "foo", :password => "bar", :num_statuses => 0 }) }
42
+ end
43
+
44
+ should "use default page number if not configured otherwise" do
45
+ @client = Client.new(@deps, { :username => "foo", :password => "bar" })
46
+ assert_equal Client::DEFAULT_PAGE_NUM, @client.page_num
47
+ end
48
+
49
+ should "use configured page number if in allowed range" do
50
+ @client = Client.new(@deps, { :username => "foo", :password => "bar", :page_num => 12 })
51
+ assert_equal 12, @client.page_num
52
+ end
53
+
54
+ should "raise an exception for configured page number if not in allowed range" do
55
+ assert_raise(ArgumentError) { Client.new(@deps, { :username => "foo", :password => "bar", :page_num => 0 }) }
56
+ end
57
+ end
58
+
59
+ context "at runtime" do
60
+ setup do
61
+ @username = "spiky"
62
+ @password = "lullaby"
63
+ @client = Client.new(@deps, { :username => @username, :password => @password })
64
+ @base_url = "https://#{@username}:#{@password}@twitter.com"
65
+ @statuses_query_params = "count=#{Client::DEFAULT_NUM_STATUSES}&page=#{Client::DEFAULT_PAGE_NUM}"
66
+ @users_query_params = "page=#{Client::DEFAULT_PAGE_NUM}"
67
+ end
68
+
69
+ should "fetch friends' statuses (home view)" do
70
+ status_records, gen_records = create_test_statuses(
71
+ {
72
+ :user => "zanzibar",
73
+ :status => {
74
+ :created_at => Time.at(1).to_s,
75
+ :text => "wassup?",
76
+ :in_reply_to => nil
77
+ }
78
+ },
79
+ {
80
+ :user => "lulzwoo",
81
+ :status => {
82
+ :created_at => Time.at(1).to_s,
83
+ :text => "nuttin'",
84
+ :in_reply_to => nil
85
+ }
86
+ }
87
+ )
88
+ @rest_client.expects(:get) \
89
+ .with("#{@base_url}/statuses/friends_timeline.json?#{@statuses_query_params}") \
90
+ .returns(status_records.to_json)
91
+ @io.expects(:show_record).with(gen_records[0])
92
+ @io.expects(:show_record).with(gen_records[1])
93
+ @client.home
94
+ end
95
+
96
+ should "fetch mentions" do
97
+ status_records, gen_records = create_test_statuses(
98
+ {
99
+ :user => "zanzibar",
100
+ :status => {
101
+ :created_at => Time.at(1).to_s,
102
+ :text => "wassup, @#{@username}?",
103
+ :in_reply_to => @username
104
+ }
105
+ },
106
+ {
107
+ :user => "lulzwoo",
108
+ :status => {
109
+ :created_at => Time.at(1).to_s,
110
+ :text => "@#{@username}, doing nuttin'",
111
+ :in_reply_to => @username
112
+ }
113
+ }
114
+ )
115
+ @rest_client.expects(:get) \
116
+ .with("#{@base_url}/statuses/mentions.json?#{@statuses_query_params}") \
117
+ .returns(status_records.to_json)
118
+ @io.expects(:show_record).with(gen_records[0])
119
+ @io.expects(:show_record).with(gen_records[1])
120
+ @client.mentions
121
+ end
122
+
123
+ should "fetch a specific user's statuses, when the user identified by given argument" do
124
+ user = "spoonman"
125
+ status_records, gen_records = create_test_statuses(
126
+ {
127
+ :user => user,
128
+ :status => {
129
+ :created_at => Time.at(1).to_s,
130
+ :text => "wassup?",
131
+ :in_reply_to => nil
132
+ }
133
+ }
134
+ )
135
+ @rest_client.expects(:get) \
136
+ .with("#{@base_url}/statuses/user_timeline/#{user}.json?#{@statuses_query_params}") \
137
+ .returns(status_records.to_json)
138
+ @io.expects(:show_record).with(gen_records[0])
139
+ @client.user(user)
140
+ end
141
+
142
+ should "fetch a specific user's statuses, with the user being the authenticated user itself when given no argument" do
143
+ status_records, gen_records = create_test_statuses(
144
+ {
145
+ :user => @username,
146
+ :status => {
147
+ :created_at => Time.at(1).to_s,
148
+ :text => "wassup?",
149
+ :in_reply_to => nil
150
+ }
151
+ }
152
+ )
153
+ @rest_client.expects(:get) \
154
+ .with("#{@base_url}/statuses/user_timeline/#{@username}.json?#{@statuses_query_params}") \
155
+ .returns(status_records.to_json)
156
+ @io.expects(:show_record).with(gen_records[0])
157
+ @client.user
158
+ end
159
+
160
+ context "for posting status updates" do
161
+ should "post a status update via argument, when positive confirmation" do
162
+ status = "wondering around"
163
+ status_records, gen_records = create_test_statuses(
164
+ {
165
+ :user => @username,
166
+ :status => {
167
+ :created_at => Time.at(1).to_s,
168
+ :text => status,
169
+ :in_reply_to => nil
170
+ }
171
+ }
172
+ )
173
+ @rest_client.expects(:post) \
174
+ .with("#{@base_url}/statuses/update.json", {:status => status}) \
175
+ .returns(status_records[0].to_json)
176
+ @io.expects(:confirm).with("Really send?").returns(true)
177
+ @io.expects(:show_status_preview).with(status)
178
+ @io.expects(:info).with("Sent status update.\n\n")
179
+ @io.expects(:show_record).with(gen_records[0])
180
+ @client.update(status)
181
+ end
182
+
183
+ should "post a status update via prompt, when positive confirmation" do
184
+ status = "wondering around"
185
+ status_records, gen_records = create_test_statuses(
186
+ { :user => @username,
187
+ :status => {
188
+ :created_at => Time.at(1).to_s,
189
+ :text => status,
190
+ :in_reply_to => nil
191
+ }
192
+ }
193
+ )
194
+ @rest_client.expects(:post) \
195
+ .with("#{@base_url}/statuses/update.json", {:status => status}) \
196
+ .returns(status_records[0].to_json)
197
+ @io.expects(:prompt).with("Status update").returns(status)
198
+ @io.expects(:show_status_preview).with(status)
199
+ @io.expects(:confirm).with("Really send?").returns(true)
200
+ @io.expects(:info).with("Sent status update.\n\n")
201
+ @io.expects(:show_record).with(gen_records[0])
202
+ @client.update
203
+ end
204
+
205
+ should "cancel a status update via argument, when negative confirmation" do
206
+ status = "wondering around"
207
+ @rest_client.expects(:post).never
208
+ @io.expects(:show_status_preview).with(status)
209
+ @io.expects(:confirm).with("Really send?").returns(false)
210
+ @io.expects(:info).with("Cancelled.")
211
+ @io.expects(:show_record).never
212
+ @client.update(status)
213
+ end
214
+
215
+ should "cancel a status update via prompt, when negative confirmation" do
216
+ status = "wondering around"
217
+ @rest_client.expects(:post).never
218
+ @io.expects(:prompt).with("Status update").returns(status)
219
+ @io.expects(:show_status_preview).with(status)
220
+ @io.expects(:confirm).with("Really send?").returns(false)
221
+ @io.expects(:info).with("Cancelled.")
222
+ @io.expects(:show_record).never
223
+ @client.update
224
+ end
225
+
226
+ should "cancel a status update via argument, when empty status" do
227
+ @rest_client.expects(:post).never
228
+ @io.expects(:confirm).never
229
+ @io.expects(:info).with("Cancelled.")
230
+ @io.expects(:show_record).never
231
+ @client.update("")
232
+ end
233
+
234
+ should "cancel a status update via prompt, when empty status" do
235
+ @rest_client.expects(:post).never
236
+ @io.expects(:prompt).with("Status update").returns("")
237
+ @io.expects(:confirm).never
238
+ @io.expects(:info).with("Cancelled.")
239
+ @io.expects(:show_record).never
240
+ @client.update
241
+ end
242
+
243
+ should "remove excess whitespace around a status update" do
244
+ whitespaced_status = " oh, i was sloppy \t "
245
+ stripped_status = "oh, i was sloppy"
246
+ status_records, gen_records = create_test_statuses(
247
+ { :user => @username,
248
+ :status => {
249
+ :created_at => Time.at(1).to_s,
250
+ :text => stripped_status,
251
+ :in_reply_to => nil
252
+ }
253
+ }
254
+ )
255
+ @rest_client.expects(:post) \
256
+ .with("#{@base_url}/statuses/update.json", {:status => stripped_status}) \
257
+ .returns(status_records[0].to_json)
258
+ @io.expects(:show_status_preview).with(stripped_status)
259
+ @io.expects(:confirm).with("Really send?").returns(true)
260
+ @io.expects(:info).with("Sent status update.\n\n")
261
+ @io.expects(:show_record).with(gen_records[0])
262
+ @client.update(whitespaced_status)
263
+ end
264
+
265
+ should "truncate a status update with too long argument and warn the user" do
266
+ long_status = "x aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn ooo ppp qqq rrr sss ttt uuu vvv www xxx yyy zzz 111 222 333 444 555 666 777 888 999 000"
267
+ truncated_status = "x aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn ooo ppp qqq rrr sss ttt uuu vvv www xxx yyy zzz 111 222 333 444 555 666 777 888 99"
268
+ status_records, gen_records = create_test_statuses(
269
+ { :user => @username,
270
+ :status => {
271
+ :created_at => Time.at(1).to_s,
272
+ :text => truncated_status,
273
+ :in_reply_to => nil
274
+ }
275
+ }
276
+ )
277
+ @rest_client.expects(:post) \
278
+ .with("#{@base_url}/statuses/update.json", {:status => truncated_status}) \
279
+ .returns(status_records[0].to_json)
280
+ @io.expects(:warn).with("Status will be truncated.")
281
+ @io.expects(:show_status_preview).with(truncated_status)
282
+ @io.expects(:confirm).with("Really send?").returns(true)
283
+ @io.expects(:info).with("Sent status update.\n\n")
284
+ @io.expects(:show_record).with(gen_records[0])
285
+ @client.update(long_status)
286
+ end
287
+
288
+ context "with URL shortening enabled" do
289
+ setup do
290
+ @client = Client.new(@deps, {
291
+ :username => @username,
292
+ :password => @password,
293
+ :shorten_urls => {
294
+ :enable => true,
295
+ :service_url => "http://shorten.it/create",
296
+ :method => "post",
297
+ :url_param_name => "url",
298
+ :xpath_selector => "//input[@id='short_url']/@value"
299
+ }
300
+ })
301
+ end
302
+
303
+ should "shorten URLs, avoiding truncation with long URLs" do
304
+ long_urls = ["http://www.google.fi/search?q=ruby+nokogiri&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-US:official&client=firefox-a", "http://www.w3.org/TR/1999/REC-xpath-19991116"]
305
+ long_status = long_urls.join(" and ")
306
+ short_urls = ["http://shorten.it/2k7i8", "http://shorten.it/2k7mk"]
307
+ shortened_status = short_urls.join(" and ")
308
+ status_records, gen_records = create_test_statuses(
309
+ { :user => @username,
310
+ :status => {
311
+ :created_at => Time.at(1).to_s,
312
+ :text => shortened_status,
313
+ :in_reply_to => nil
314
+ }
315
+ }
316
+ )
317
+ @rest_client.expects(:post) \
318
+ .with("#{@base_url}/statuses/update.json", {:status => shortened_status}) \
319
+ .returns(status_records[0].to_json)
320
+ @url_shortener.expects(:shorten).with(long_urls.first).returns(short_urls.first)
321
+ @url_shortener.expects(:shorten).with(long_urls.last).returns(short_urls.last)
322
+ @io.expects(:show_status_preview).with(shortened_status)
323
+ @io.expects(:confirm).with("Really send?").returns(true)
324
+ @io.expects(:info).with("Sent status update.\n\n")
325
+ @io.expects(:show_record).with(gen_records[0])
326
+ @client.update(long_status)
327
+ end
328
+
329
+ should "discard obviously invalid shortened URLs, using originals instead" do
330
+ long_urls = ["http://www.google.fi/", "http://www.w3.org/TR/1999/REC-xpath-19991116"]
331
+ status = long_urls.join(" and ")
332
+ short_urls = [nil, ""]
333
+ status_records, gen_records = create_test_statuses(
334
+ { :user => @username,
335
+ :status => {
336
+ :created_at => Time.at(1).to_s,
337
+ :text => status,
338
+ :in_reply_to => nil
339
+ }
340
+ }
341
+ )
342
+ @rest_client.expects(:post) \
343
+ .with("#{@base_url}/statuses/update.json", {:status => status}) \
344
+ .returns(status_records[0].to_json)
345
+ @url_shortener.expects(:shorten).with(long_urls.first).returns(short_urls.first)
346
+ @url_shortener.expects(:shorten).with(long_urls.last).returns(short_urls.last)
347
+ @io.expects(:show_status_preview).with(status)
348
+ @io.expects(:confirm).with("Really send?").returns(true)
349
+ @io.expects(:info).with("Sent status update.\n\n")
350
+ @io.expects(:show_record).with(gen_records[0])
351
+ @client.update(status)
352
+ end
353
+
354
+ should "reuse a shortened URL for duplicate long URLs" do
355
+ long_urls = ["http://www.w3.org/TR/1999/REC-xpath-19991116"] * 2
356
+ long_status = long_urls.join(" and ")
357
+ short_url = "http://shorten.it/2k7mk"
358
+ short_status = ([short_url] * 2).join(" and ")
359
+ status_records, gen_records = create_test_statuses(
360
+ { :user => @username,
361
+ :status => {
362
+ :created_at => Time.at(1).to_s,
363
+ :text => short_status,
364
+ :in_reply_to => nil
365
+ }
366
+ }
367
+ )
368
+ @rest_client.expects(:post) \
369
+ .with("#{@base_url}/statuses/update.json", {:status => short_status}) \
370
+ .returns(status_records[0].to_json)
371
+ @url_shortener.expects(:shorten).with(long_urls.first).returns(short_url)
372
+ @io.expects(:show_status_preview).with(short_status)
373
+ @io.expects(:confirm).with("Really send?").returns(true)
374
+ @io.expects(:info).with("Sent status update.\n\n")
375
+ @io.expects(:show_record).with(gen_records[0])
376
+ @client.update(long_status)
377
+ end
378
+
379
+ context "in erroneous situations" do
380
+ setup do
381
+ @url = "http://www.w3.org/TR/1999/REC-xpath-19991116"
382
+ @status = "skimming through #{@url}"
383
+ @status_records, @gen_records = create_test_statuses(
384
+ { :user => @username,
385
+ :status => {
386
+ :created_at => Time.at(1).to_s,
387
+ :text => @status,
388
+ :in_reply_to => nil
389
+ }
390
+ }
391
+ )
392
+ end
393
+
394
+ should "skip shortening URLs if required libraries are not found" do
395
+ @rest_client.expects(:post) \
396
+ .with("#{@base_url}/statuses/update.json", {:status => @status}) \
397
+ .returns(@status_records[0].to_json)
398
+ @url_shortener.expects(:shorten).with(@url).raises(LoadError, "gem not found")
399
+ @io.expects(:warn)
400
+ @io.expects(:show_status_preview).with(@status)
401
+ @io.expects(:confirm).with("Really send?").returns(true)
402
+ @io.expects(:info).with("Sent status update.\n\n")
403
+ @io.expects(:show_record).with(@gen_records[0])
404
+ @client.update(@status)
405
+ end
406
+
407
+ should "skip shortening URLs upon connection error to the URL shortening service" do
408
+ @rest_client.expects(:post) \
409
+ .with("#{@base_url}/statuses/update.json", {:status => @status}) \
410
+ .returns(@status_records[0].to_json)
411
+ @url_shortener.expects(:shorten).with(@url).raises(ClientError, "connection error")
412
+ @io.expects(:warn)
413
+ @io.expects(:show_status_preview).with(@status)
414
+ @io.expects(:confirm).with("Really send?").returns(true)
415
+ @io.expects(:info).with("Sent status update.\n\n")
416
+ @io.expects(:show_record).with(@gen_records[0])
417
+ @client.update(@status)
418
+ end
419
+ end
420
+ end
421
+ end
422
+
423
+ should "fetch friends" do
424
+ user_records, gen_records = create_test_users(
425
+ {
426
+ :user => "zanzibar",
427
+ :status => {
428
+ :created_at => Time.at(1).to_s,
429
+ :text => "wassup, @foo?",
430
+ :in_reply_to => "foo"
431
+ }
432
+ },
433
+ {
434
+ :user => "lulzwoo",
435
+ :status => {
436
+ :created_at => Time.at(1).to_s,
437
+ :text => "@foo, doing nuttin'",
438
+ :in_reply_to => "foo"
439
+ }
440
+ }
441
+ )
442
+ @rest_client.expects(:get) \
443
+ .with("#{@base_url}/statuses/friends/#{@username}.json?#{@users_query_params}") \
444
+ .returns(user_records.to_json)
445
+ @io.expects(:show_record).with(gen_records[0])
446
+ @io.expects(:show_record).with(gen_records[1])
447
+ @client.friends
448
+ end
449
+
450
+ should "fetch followers" do
451
+ user_records, gen_records = create_test_users(
452
+ {
453
+ :user => "zanzibar",
454
+ :status => {
455
+ :created_at => Time.at(1).to_s,
456
+ :text => "wassup, @foo?",
457
+ :in_reply_to => "foo"
458
+ }
459
+ },
460
+ {
461
+ :user => "lulzwoo"
462
+ }
463
+ )
464
+ @rest_client.expects(:get) \
465
+ .with("#{@base_url}/statuses/followers/#{@username}.json?#{@users_query_params}") \
466
+ .returns(user_records.to_json)
467
+ @io.expects(:show_record).with(gen_records[0])
468
+ @io.expects(:show_record).with(gen_records[1])
469
+ @client.followers
470
+ end
471
+ end
472
+ end
473
+ end
474
+
475
+ end