tweetwine 0.2.12 → 0.3.0

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.
Files changed (67) hide show
  1. data/CHANGELOG.rdoc +7 -0
  2. data/Gemfile +17 -0
  3. data/README.md +57 -47
  4. data/Rakefile +17 -26
  5. data/bin/tweetwine +11 -12
  6. data/contrib/tweetwine-completion.bash +2 -3
  7. data/example/application_behavior_example.rb +173 -0
  8. data/example/example_helper.rb +44 -28
  9. data/example/fixture/config.yaml +8 -0
  10. data/example/fixture/shorten_rubygems.html +5 -0
  11. data/example/fixture/shorten_rubylang.html +5 -0
  12. data/example/fixture/update_utf8.json +1 -0
  13. data/example/fixture/update_with_urls.json +1 -0
  14. data/example/fixture/{update.json → update_without_urls.json} +0 -0
  15. data/example/search_statuses_example.rb +49 -16
  16. data/example/show_followers_example.rb +7 -8
  17. data/example/show_friends_example.rb +7 -8
  18. data/example/show_home_example.rb +19 -16
  19. data/example/show_mentions_example.rb +8 -9
  20. data/example/show_user_example.rb +16 -13
  21. data/example/update_status_example.rb +143 -26
  22. data/example/use_http_proxy_example.rb +40 -20
  23. data/lib/tweetwine/basic_object.rb +19 -0
  24. data/lib/tweetwine/character_encoding.rb +59 -0
  25. data/lib/tweetwine/cli.rb +354 -230
  26. data/lib/tweetwine/config.rb +65 -0
  27. data/lib/tweetwine/http.rb +120 -0
  28. data/lib/tweetwine/oauth.rb +104 -0
  29. data/lib/tweetwine/obfuscate.rb +21 -0
  30. data/lib/tweetwine/option_parser.rb +31 -0
  31. data/lib/tweetwine/promise.rb +39 -0
  32. data/lib/tweetwine/twitter.rb +211 -0
  33. data/lib/tweetwine/{io.rb → ui.rb} +30 -21
  34. data/lib/tweetwine/url_shortener.rb +15 -9
  35. data/lib/tweetwine/util.rb +30 -15
  36. data/lib/tweetwine.rb +72 -12
  37. data/man/tweetwine.7 +43 -69
  38. data/man/tweetwine.7.ronn +57 -47
  39. data/test/character_encoding_test.rb +87 -0
  40. data/test/cli_test.rb +19 -6
  41. data/test/config_test.rb +244 -0
  42. data/test/fixture/oauth.rb +21 -0
  43. data/test/fixture/test_config.yaml +4 -4
  44. data/test/http_test.rb +199 -0
  45. data/test/oauth_test.rb +77 -0
  46. data/test/obfuscate_test.rb +16 -0
  47. data/test/option_parser_test.rb +60 -0
  48. data/test/promise_test.rb +56 -0
  49. data/test/test_helper.rb +76 -8
  50. data/test/twitter_test.rb +625 -0
  51. data/test/{io_test.rb → ui_test.rb} +92 -74
  52. data/test/url_shortener_test.rb +115 -135
  53. data/test/util_test.rb +136 -85
  54. data/tweetwine.gemspec +53 -0
  55. metadata +112 -56
  56. data/example/show_metadata_example.rb +0 -86
  57. data/lib/tweetwine/client.rb +0 -187
  58. data/lib/tweetwine/meta.rb +0 -5
  59. data/lib/tweetwine/options.rb +0 -24
  60. data/lib/tweetwine/retrying_http.rb +0 -99
  61. data/lib/tweetwine/startup_config.rb +0 -50
  62. data/man/tweetwine.1 +0 -109
  63. data/man/tweetwine.1.ronn +0 -69
  64. data/test/client_test.rb +0 -544
  65. data/test/options_test.rb +0 -45
  66. data/test/retrying_http_test.rb +0 -147
  67. data/test/startup_config_test.rb +0 -162
@@ -0,0 +1,625 @@
1
+ # coding: utf-8
2
+
3
+ require "test_helper"
4
+ require "json"
5
+
6
+ module Tweetwine::Test
7
+
8
+ class ClientTest < UnitTestCase
9
+ context "for initialization" do
10
+ should "use default number of statuses if not configured" do
11
+ @twitter = Twitter.new
12
+ assert_equal Twitter::DEFAULT_NUM_STATUSES, @twitter.num_statuses
13
+ end
14
+
15
+ should "use configured number of statuses if in allowed range" do
16
+ @twitter = Twitter.new(:num_statuses => 12)
17
+ assert_equal 12, @twitter.num_statuses
18
+ end
19
+
20
+ should "raise exception if configured number of status not in allowed range" do
21
+ assert_raise(ArgumentError) { Twitter.new(:num_statuses => 0) }
22
+ end
23
+
24
+ should "use default page number if not configured otherwise" do
25
+ @twitter = Twitter.new
26
+ assert_equal Twitter::DEFAULT_PAGE_NUM, @twitter.page
27
+ end
28
+
29
+ should "use configured page number if in allowed range" do
30
+ @twitter = Twitter.new(:page => 12)
31
+ assert_equal 12, @twitter.page
32
+ end
33
+
34
+ should "raise exception if configured page number not in allowed range" do
35
+ assert_raise(ArgumentError) { Twitter.new(:page => 0) }
36
+ end
37
+ end
38
+
39
+ context "at runtime" do
40
+ setup do
41
+ mock_http
42
+ mock_oauth
43
+ mock_ui
44
+ @username = "spiky"
45
+ @rest_api = mock("Http's REST API")
46
+ @search_api = mock("Http's Search API")
47
+ @http.stubs(:as_resource).with("https://api.twitter.com/1").returns(@rest_api)
48
+ @http.stubs(:as_resource).with("http://search.twitter.com").returns(@search_api)
49
+ @twitter = Twitter.new(:username => @username)
50
+ @rest_api_status_query_str = "count=#{Twitter::DEFAULT_NUM_STATUSES}&page=#{Twitter::DEFAULT_PAGE_NUM}"
51
+ @search_api_query_str = "page=#{Twitter::DEFAULT_PAGE_NUM}&rpp=#{Twitter::DEFAULT_NUM_STATUSES}"
52
+ end
53
+
54
+ should "fetch friends' statuses (home view)" do
55
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api(
56
+ {
57
+ :from_user => "zanzibar",
58
+ :status => "wassup?",
59
+ :created_at => Time.at(1).to_s,
60
+ :to_user => nil
61
+ },
62
+ {
63
+ :from_user => "lulzwoo",
64
+ :status => "nuttin'",
65
+ :created_at => Time.at(1).to_s,
66
+ :to_user => nil
67
+ })
68
+ @oauth.expects(:request_signer)
69
+ @rest_api.expects(:[]).
70
+ with("statuses/home_timeline.json?#{@rest_api_status_query_str}").
71
+ returns(stub(:get => twitter_records.to_json))
72
+ @ui.expects(:show_record).with(internal_records[0])
73
+ @ui.expects(:show_record).with(internal_records[1])
74
+ @twitter.home
75
+ end
76
+
77
+ should "fetch mentions" do
78
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api(
79
+ {
80
+ :from_user => "zanzibar",
81
+ :status => "wassup, @#{@username}?",
82
+ :created_at => Time.at(1).to_s,
83
+ :to_user => @username
84
+ },
85
+ {
86
+ :from_user => "lulzwoo",
87
+ :status => "@#{@username}, doing nuttin'",
88
+ :created_at => Time.at(1).to_s,
89
+ :to_user => @username
90
+ })
91
+ @oauth.expects(:request_signer)
92
+ @rest_api.expects(:[]).
93
+ with("statuses/mentions.json?#{@rest_api_status_query_str}").
94
+ returns(stub(:get => twitter_records.to_json))
95
+ @ui.expects(:show_record).with(internal_records[0])
96
+ @ui.expects(:show_record).with(internal_records[1])
97
+ @twitter.mentions
98
+ end
99
+
100
+ should "fetch a specific user's statuses, when user is given as argument" do
101
+ username = "spoonman"
102
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
103
+ :from_user => username,
104
+ :status => "wassup?",
105
+ :created_at => Time.at(1).to_s,
106
+ :to_user => nil
107
+ })
108
+ @oauth.expects(:request_signer)
109
+ @rest_api.expects(:[]).
110
+ with("statuses/user_timeline.json?#{@rest_api_status_query_str}&screen_name=#{username}").
111
+ returns(stub(:get => twitter_records.to_json))
112
+ @ui.expects(:show_record).with(internal_records[0])
113
+ @twitter.user(username)
114
+ end
115
+
116
+ should "fetch a specific user's statuses, with user being the authenticated user itself when given no argument" do
117
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
118
+ :from_user => @username,
119
+ :status => "wassup?",
120
+ :created_at => Time.at(1).to_s,
121
+ :to_user => nil
122
+ })
123
+ @oauth.expects(:request_signer)
124
+ @rest_api.expects(:[]).
125
+ with("statuses/user_timeline.json?#{@rest_api_status_query_str}&screen_name=#{@username}").
126
+ returns(stub(:get => twitter_records.to_json))
127
+ @ui.expects(:show_record).with(internal_records[0])
128
+ @twitter.user
129
+ end
130
+
131
+ context "when posting status updates" do
132
+ should "post a status update via argument, when positive confirmation" do
133
+ status = "wondering around"
134
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
135
+ :from_user => @username,
136
+ :status => status,
137
+ :created_at => Time.at(1).to_s,
138
+ :to_user => nil
139
+ })
140
+ @oauth.expects(:request_signer)
141
+ http_subresource = mock
142
+ http_subresource.expects(:post).
143
+ with({ :status => status }).
144
+ returns(twitter_records[0].to_json)
145
+ @rest_api.expects(:[]).
146
+ with("statuses/update.json").
147
+ returns(http_subresource)
148
+ @ui.expects(:confirm).with("Really send?").returns(true)
149
+ @ui.expects(:show_status_preview).with(status)
150
+ @ui.expects(:info).with("Sent status update.\n\n")
151
+ @ui.expects(:show_record).with(internal_records[0])
152
+ @twitter.update(status)
153
+ end
154
+
155
+ should "post a status update via prompt, when positive confirmation" do
156
+ status = "wondering around"
157
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
158
+ :from_user => @username,
159
+ :status => status,
160
+ :created_at => Time.at(1).to_s,
161
+ :to_user => nil
162
+ })
163
+ @oauth.expects(:request_signer)
164
+ http_subresource = mock
165
+ http_subresource.expects(:post).
166
+ with({ :status => status }).
167
+ returns(twitter_records[0].to_json)
168
+ @rest_api.expects(:[]).
169
+ with("statuses/update.json").
170
+ returns(http_subresource)
171
+ @ui.expects(:prompt).with("Status update").returns(status)
172
+ @ui.expects(:show_status_preview).with(status)
173
+ @ui.expects(:confirm).with("Really send?").returns(true)
174
+ @ui.expects(:info).with("Sent status update.\n\n")
175
+ @ui.expects(:show_record).with(internal_records[0])
176
+ @twitter.update
177
+ end
178
+
179
+ should "cancel a status update via argument, when negative confirmation" do
180
+ status = "wondering around"
181
+ @rest_api.expects(:[]).never
182
+ @ui.expects(:show_status_preview).with(status)
183
+ @ui.expects(:confirm).with("Really send?").returns(false)
184
+ @ui.expects(:info).with("Cancelled.")
185
+ @ui.expects(:show_record).never
186
+ @twitter.update(status)
187
+ end
188
+
189
+ should "cancel a status update via prompt, when negative confirmation" do
190
+ status = "wondering around"
191
+ @rest_api.expects(:[]).never
192
+ @ui.expects(:prompt).with("Status update").returns(status)
193
+ @ui.expects(:show_status_preview).with(status)
194
+ @ui.expects(:confirm).with("Really send?").returns(false)
195
+ @ui.expects(:info).with("Cancelled.")
196
+ @ui.expects(:show_record).never
197
+ @twitter.update
198
+ end
199
+
200
+ should "cancel a status update via argument, when empty status" do
201
+ @rest_api.expects(:[]).never
202
+ @ui.expects(:prompt).with("Status update").returns("")
203
+ @ui.expects(:confirm).never
204
+ @ui.expects(:info).with("Cancelled.")
205
+ @ui.expects(:show_record).never
206
+ @twitter.update("")
207
+ end
208
+
209
+ should "cancel a status update via prompt, when empty status" do
210
+ @rest_api.expects(:[]).never
211
+ @ui.expects(:prompt).with("Status update").returns("")
212
+ @ui.expects(:confirm).never
213
+ @ui.expects(:info).with("Cancelled.")
214
+ @ui.expects(:show_record).never
215
+ @twitter.update
216
+ end
217
+
218
+ should "remove excess whitespace around a status update" do
219
+ whitespaced_status = " oh, i was sloppy \t "
220
+ stripped_status = "oh, i was sloppy"
221
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
222
+ :from_user => @username,
223
+ :status => stripped_status,
224
+ :created_at => Time.at(1).to_s,
225
+ :to_user => nil
226
+ })
227
+ @oauth.expects(:request_signer)
228
+ http_subresource = mock
229
+ http_subresource.expects(:post).
230
+ with({ :status => stripped_status }).
231
+ returns(twitter_records[0].to_json)
232
+ @rest_api.expects(:[]).
233
+ with("statuses/update.json").
234
+ returns(http_subresource)
235
+ @ui.expects(:show_status_preview).with(stripped_status)
236
+ @ui.expects(:confirm).with("Really send?").returns(true)
237
+ @ui.expects(:info).with("Sent status update.\n\n")
238
+ @ui.expects(:show_record).with(internal_records[0])
239
+ @twitter.update(whitespaced_status)
240
+ end
241
+
242
+ should "truncate a status update with too long argument and warn the user" do
243
+ truncated_status = "ab c" * 35 # 4 * 35 = 140
244
+ long_status = "#{truncated_status} dd"
245
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
246
+ :from_user => @username,
247
+ :status => truncated_status,
248
+ :created_at => Time.at(1).to_s,
249
+ :to_user => nil
250
+ })
251
+ @oauth.expects(:request_signer)
252
+ http_subresource = mock
253
+ http_subresource.expects(:post).
254
+ with({ :status => truncated_status }).
255
+ returns(twitter_records[0].to_json)
256
+ @rest_api.expects(:[]).
257
+ with("statuses/update.json").
258
+ returns(http_subresource)
259
+ @ui.expects(:warn).with("Status will be truncated.")
260
+ @ui.expects(:show_status_preview).with(truncated_status)
261
+ @ui.expects(:confirm).with("Really send?").returns(true)
262
+ @ui.expects(:info).with("Sent status update.\n\n")
263
+ @ui.expects(:show_record).with(internal_records[0])
264
+ @twitter.update(long_status)
265
+ end
266
+
267
+ if "".respond_to?(:encode)
268
+ should "encode status in UTF-8 (String supports encoding)" do
269
+ status_utf8, status_latin1 = "résumé", "résumé".encode('ISO-8859-1')
270
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
271
+ :from_user => @username,
272
+ :status => status_utf8,
273
+ :created_at => Time.at(1).to_s,
274
+ :to_user => nil
275
+ })
276
+ @oauth.expects(:request_signer)
277
+ http_subresource = mock
278
+ http_subresource.expects(:post).
279
+ with({ :status => status_utf8 }).
280
+ returns(twitter_records[0].to_json)
281
+ @rest_api.expects(:[]).
282
+ with("statuses/update.json").
283
+ returns(http_subresource)
284
+ @ui.expects(:confirm).with("Really send?").returns(true)
285
+ @ui.expects(:show_status_preview).with(status_latin1)
286
+ @ui.expects(:info).with("Sent status update.\n\n")
287
+ @ui.expects(:show_record).with(internal_records[0])
288
+ @twitter.update(status_latin1)
289
+ end
290
+ else
291
+ should "encode status in UTF-8 (String does not support encoding)" do
292
+ tmp_kcode('NONE') do
293
+ tmp_env(:LANG => 'ISO-8859-1') do
294
+ status_utf8, status_latin1 = "r\xc3\xa9sum\xc3\xa9", "r\xe9sum\xe9"
295
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
296
+ :from_user => @username,
297
+ :status => status_utf8,
298
+ :created_at => Time.at(1).to_s,
299
+ :to_user => nil
300
+ })
301
+ @oauth.expects(:request_signer)
302
+ http_subresource = mock
303
+ http_subresource.expects(:post).
304
+ with({ :status => status_utf8 }).
305
+ returns(twitter_records[0].to_json)
306
+ @rest_api.expects(:[]).
307
+ with("statuses/update.json").
308
+ returns(http_subresource)
309
+ @ui.expects(:confirm).with("Really send?").returns(true)
310
+ @ui.expects(:show_status_preview).with(status_latin1)
311
+ @ui.expects(:info).with("Sent status update.\n\n")
312
+ @ui.expects(:show_record).with(internal_records[0])
313
+ @twitter.update(status_latin1)
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ context "with URL shortening" do
320
+ setup do
321
+ mock_url_shortener
322
+ stub_config(
323
+ :shorten_urls => {
324
+ :service_url => "http://shorten.it/create",
325
+ :method => "post",
326
+ :url_param_name => "url",
327
+ :xpath_selector => "//input[@id='short_url']/@value"
328
+ })
329
+ end
330
+
331
+ should "not shorten URLs if not configured" do
332
+ stub_config
333
+ status = "reading http://www.w3.org/TR/1999/REC-xpath-19991116"
334
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
335
+ :from_user => @username,
336
+ :status => status,
337
+ :created_at => Time.at(1).to_s,
338
+ :to_user => nil
339
+ })
340
+ @oauth.expects(:request_signer)
341
+ http_subresource = mock
342
+ http_subresource.expects(:post).
343
+ with({ :status => status }).
344
+ returns(twitter_records[0].to_json)
345
+ @url_shortener.expects(:shorten).never
346
+ @rest_api.expects(:[]).
347
+ with("statuses/update.json").
348
+ returns(http_subresource)
349
+ @ui.expects(:confirm).with("Really send?").returns(true)
350
+ @ui.expects(:show_status_preview).with(status)
351
+ @ui.expects(:info).with("Sent status update.\n\n")
352
+ @ui.expects(:show_record).with(internal_records[0])
353
+ @twitter.update(status)
354
+ end
355
+
356
+ should "shorten URLs, avoiding truncation with long URLs" do
357
+ 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"]
358
+ long_status = long_urls.join(" and ")
359
+ short_urls = ["http://shorten.it/2k7i8", "http://shorten.it/2k7mk"]
360
+ shortened_status = short_urls.join(" and ")
361
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
362
+ :from_user => @username,
363
+ :status => shortened_status,
364
+ :created_at => Time.at(1).to_s,
365
+ :to_user => nil
366
+ })
367
+ @oauth.expects(:request_signer)
368
+ http_subresource = mock
369
+ http_subresource.expects(:post).
370
+ with({ :status => shortened_status }).
371
+ returns(twitter_records[0].to_json)
372
+ @rest_api.expects(:[]).
373
+ with("statuses/update.json").
374
+ returns(http_subresource)
375
+ @url_shortener.expects(:shorten).with(long_urls.first).returns(short_urls.first)
376
+ @url_shortener.expects(:shorten).with(long_urls.last).returns(short_urls.last)
377
+ @ui.expects(:show_status_preview).with(shortened_status)
378
+ @ui.expects(:confirm).with("Really send?").returns(true)
379
+ @ui.expects(:info).with("Sent status update.\n\n")
380
+ @ui.expects(:show_record).with(internal_records[0])
381
+ @twitter.update(long_status)
382
+ end
383
+
384
+ should "discard obviously invalid shortened URLs, using originals instead" do
385
+ long_urls = ["http://www.google.fi/", "http://www.w3.org/TR/1999/REC-xpath-19991116"]
386
+ status = long_urls.join(" and ")
387
+ short_urls = [nil, ""]
388
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
389
+ :from_user => @username,
390
+ :status => status,
391
+ :created_at => Time.at(1).to_s,
392
+ :to_user => nil
393
+ })
394
+ @oauth.expects(:request_signer)
395
+ http_subresource = mock
396
+ http_subresource.expects(:post).
397
+ with({ :status => status }).
398
+ returns(twitter_records[0].to_json)
399
+ @rest_api.expects(:[]).
400
+ with("statuses/update.json").
401
+ returns(http_subresource)
402
+ @url_shortener.expects(:shorten).with(long_urls.first).returns(short_urls.first)
403
+ @url_shortener.expects(:shorten).with(long_urls.last).returns(short_urls.last)
404
+ @ui.expects(:show_status_preview).with(status)
405
+ @ui.expects(:confirm).with("Really send?").returns(true)
406
+ @ui.expects(:info).with("Sent status update.\n\n")
407
+ @ui.expects(:show_record).with(internal_records[0])
408
+ @twitter.update(status)
409
+ end
410
+
411
+ should "reuse a shortened URL for duplicate long URLs" do
412
+ long_urls = ["http://www.w3.org/TR/1999/REC-xpath-19991116"] * 2
413
+ long_status = long_urls.join(" and ")
414
+ short_url = "http://shorten.it/2k7mk"
415
+ short_status = ([short_url] * 2).join(" and ")
416
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
417
+ :from_user => @username,
418
+ :status => short_status,
419
+ :created_at => Time.at(1).to_s,
420
+ :to_user => nil
421
+ })
422
+ @oauth.expects(:request_signer)
423
+ http_subresource = mock
424
+ http_subresource.expects(:post).
425
+ with({ :status => short_status }).
426
+ returns(twitter_records[0].to_json)
427
+ @rest_api.expects(:[]).
428
+ with("statuses/update.json").
429
+ returns(http_subresource)
430
+ @url_shortener.expects(:shorten).with(long_urls.first).returns(short_url)
431
+ @ui.expects(:show_status_preview).with(short_status)
432
+ @ui.expects(:confirm).with("Really send?").returns(true)
433
+ @ui.expects(:info).with("Sent status update.\n\n")
434
+ @ui.expects(:show_record).with(internal_records[0])
435
+ @twitter.update(long_status)
436
+ end
437
+
438
+ context "in erroneous situations" do
439
+ setup do
440
+ @url = "http://www.w3.org/TR/1999/REC-xpath-19991116"
441
+ @status = "skimming through #{@url}"
442
+ @twitter_records, @internal_records = create_test_twitter_status_records_from_rest_api({
443
+ :from_user => @username,
444
+ :status => @status,
445
+ :created_at => Time.at(1).to_s,
446
+ :to_user => nil
447
+ })
448
+ end
449
+
450
+ should "skip shortening URLs if required libraries are not found" do
451
+ @oauth.expects(:request_signer)
452
+ http_subresource = mock
453
+ http_subresource.expects(:post).
454
+ with({ :status => @status }).
455
+ returns(@twitter_records[0].to_json)
456
+ @rest_api.expects(:[]).
457
+ with("statuses/update.json").
458
+ returns(http_subresource)
459
+ @url_shortener.expects(:shorten).with(@url).raises(LoadError, "gem not found")
460
+ @ui.expects(:warn)
461
+ @ui.expects(:show_status_preview).with(@status)
462
+ @ui.expects(:confirm).with("Really send?").returns(true)
463
+ @ui.expects(:info).with("Sent status update.\n\n")
464
+ @ui.expects(:show_record).with(@internal_records[0])
465
+ @twitter.update(@status)
466
+ end
467
+
468
+ should "skip shortening URLs upon connection error to the URL shortening service" do
469
+ @oauth.expects(:request_signer)
470
+ http_subresource = mock
471
+ http_subresource.expects(:post).
472
+ with({ :status => @status }).
473
+ returns(@twitter_records[0].to_json)
474
+ @rest_api.expects(:[]).
475
+ with("statuses/update.json").
476
+ returns(http_subresource)
477
+ @url_shortener.expects(:shorten).with(@url).raises(HttpError.new(404, "Not Found"))
478
+ @ui.expects(:warn)
479
+ @ui.expects(:show_status_preview).with(@status)
480
+ @ui.expects(:confirm).with("Really send?").returns(true)
481
+ @ui.expects(:info).with("Sent status update.\n\n")
482
+ @ui.expects(:show_record).with(@internal_records[0])
483
+ @twitter.update(@status)
484
+ end
485
+ end
486
+ end
487
+ end
488
+
489
+ should "fetch friends" do
490
+ twitter_records, internal_records = create_test_twitter_user_records_from_rest_api(
491
+ {
492
+ :from_user => "zanzibar",
493
+ :status => "wassup, @foo?",
494
+ :created_at => Time.at(1).to_s,
495
+ :to_user => "foo"
496
+ },
497
+ {
498
+ :from_user => "lulzwoo",
499
+ :status => "@foo, doing nuttin'",
500
+ :created_at => Time.at(1).to_s,
501
+ :to_user => "foo"
502
+ })
503
+ @oauth.expects(:request_signer)
504
+ @rest_api.expects(:[]).
505
+ with("statuses/friends.json?#{@rest_api_status_query_str}").
506
+ returns(stub(:get => twitter_records.to_json))
507
+ @ui.expects(:show_record).with(internal_records[0])
508
+ @ui.expects(:show_record).with(internal_records[1])
509
+ @twitter.friends
510
+ end
511
+
512
+ should "fetch followers" do
513
+ twitter_records, internal_records = create_test_twitter_user_records_from_rest_api(
514
+ {
515
+ :from_user => "zanzibar",
516
+ :status => "wassup, @foo?",
517
+ :created_at => Time.at(1).to_s,
518
+ :to_user => "foo"
519
+ },
520
+ {
521
+ :from_user => "lulzwoo",
522
+ :status => nil,
523
+ :created_at => nil,
524
+ :to_user => nil
525
+ })
526
+ @oauth.expects(:request_signer)
527
+ @rest_api.expects(:[]).
528
+ with("statuses/followers.json?#{@rest_api_status_query_str}").
529
+ returns(stub(:get => twitter_records.to_json))
530
+ @ui.expects(:show_record).with(internal_records[0])
531
+ @ui.expects(:show_record).with(internal_records[1])
532
+ @twitter.followers
533
+ end
534
+
535
+ context "when searching tweets" do
536
+ should "raise exception if no search word is given" do
537
+ assert_raise(ArgumentError) { @twitter.search }
538
+ end
539
+
540
+ [
541
+ [nil, "no operator"],
542
+ [:and, "and operator"]
543
+ ].each do |op, desc|
544
+ should "search tweets matching all the given words with #{desc}" do
545
+ twitter_response, internal_records = create_test_twitter_records_from_search_api(
546
+ {
547
+ :from_user => "zanzibar",
548
+ :status => "@foo, wassup? #greets",
549
+ :created_at => Time.at(1).to_s,
550
+ :to_user => "foo"
551
+ },
552
+ {
553
+ :from_user => "spoonman",
554
+ :status => "@foo long time no see #greets",
555
+ :created_at => Time.at(1).to_s,
556
+ :to_user => "foo"
557
+ })
558
+ @search_api.expects(:[]).
559
+ with("search.json?q=%23greets%20%40foo&#{@search_api_query_str}").
560
+ returns(stub(:get => twitter_response.to_json))
561
+ @ui.expects(:show_record).with(internal_records[0])
562
+ @ui.expects(:show_record).with(internal_records[1])
563
+ @twitter.search(["#greets", "@foo"], op)
564
+ end
565
+ end
566
+
567
+ should "search tweets matching any of the given words with or operator" do
568
+ twitter_response, internal_records = create_test_twitter_records_from_search_api(
569
+ {
570
+ :from_user => "zanzibar",
571
+ :status => "spinning around the floor #habits",
572
+ :created_at => Time.at(1).to_s,
573
+ :to_user => "foo"
574
+ },
575
+ {
576
+ :from_user => "spoonman",
577
+ :status => "drinking coffee, again #neurotic",
578
+ :created_at => Time.at(1).to_s,
579
+ :to_user => "foo"
580
+ })
581
+ @search_api.expects(:[]).
582
+ with("search.json?q=%23habits%20OR%20%23neurotic&#{@search_api_query_str}").
583
+ returns(stub(:get => twitter_response.to_json))
584
+ @ui.expects(:show_record).with(internal_records[0])
585
+ @ui.expects(:show_record).with(internal_records[1])
586
+ @twitter.search(["#habits", "#neurotic"], :or)
587
+ end
588
+ end
589
+
590
+ context "when authorization fails with HTTP 401 response" do
591
+ setup do
592
+ mock_config
593
+ end
594
+
595
+ should "authorize with OAuth and save config" do
596
+ twitter_records, internal_records = create_test_twitter_status_records_from_rest_api({
597
+ :from_user => @username,
598
+ :status => "wassup?",
599
+ :created_at => Time.at(1).to_s,
600
+ :to_user => nil
601
+ })
602
+ access_token = 'access token'
603
+ user_has_authorized = states('User has authorized?').starts_as(false)
604
+ @oauth.expects(:request_signer).twice
605
+ @oauth.expects(:authorize).
606
+ yields(access_token).
607
+ then(user_has_authorized.is(true))
608
+ http_subresource = mock
609
+ http_subresource.expects(:get).
610
+ raises(HttpError.new(401, 'Unauthorized')).
611
+ when(user_has_authorized.is(false))
612
+ http_subresource.expects(:get).
613
+ returns(twitter_records.to_json).
614
+ when(user_has_authorized.is(true))
615
+ @rest_api.expects(:[]).returns(http_subresource)
616
+ @config.expects(:[]=).with(:oauth_access, access_token)
617
+ @config.expects(:save)
618
+ @ui.expects(:show_record).with(internal_records[0])
619
+ @twitter.home
620
+ end
621
+ end
622
+ end
623
+ end
624
+
625
+ end