t 0.2.1 → 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.
@@ -1,5 +1,7 @@
1
+ require 'retryable'
1
2
  require 't/core_ext/enumerable'
2
3
  require 't/core_ext/string'
4
+ require 't/collectable'
3
5
  require 't/rcfile'
4
6
  require 'thor'
5
7
  require 'twitter'
@@ -8,6 +10,8 @@ module T
8
10
  class CLI
9
11
  class List
10
12
  class Remove < Thor
13
+ include T::Collectable
14
+
11
15
  DEFAULT_HOST = 'api.twitter.com'
12
16
  DEFAULT_PROTOCOL = 'https'
13
17
 
@@ -18,12 +22,105 @@ module T
18
22
  @rcfile = RCFile.instance
19
23
  end
20
24
 
25
+ desc "friends LIST_NAME", "Remove all friends from a list."
26
+ def friends(list_name)
27
+ list_member_ids = collect_with_cursor do |cursor|
28
+ client.list_members(list_name, :cursor => cursor, :skip_status => true, :include_entities => false)
29
+ end
30
+ friend_ids = collect_with_cursor do |cursor|
31
+ client.friend_ids(:cursor => cursor)
32
+ end
33
+ list_member_ids_to_remove = (friend_ids - list_member_ids)
34
+ number = list_member_ids_to_remove.length
35
+ if number.zero?
36
+ return say "None of @#{@rcfile.default_profile[0]}'s friends are members of the list \"#{list_name}\"."
37
+ else
38
+ return unless yes? "Are you sure you want to remove #{number} #{number == 1 ? 'friend' : 'friends'} from the list \"#{list_name}\"?"
39
+ end
40
+ list_member_ids_to_remove.threaded_map do |list_member_id|
41
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
42
+ client.list_remove_member(list_name, list_member_id)
43
+ end
44
+ end
45
+ say "@#{@rcfile.default_profile[0]} removed #{number} #{number == 1 ? 'friend' : 'friends'} from the list \"#{list_name}\"."
46
+ say
47
+ say "Run `#{$0} list add all friends #{list_name}` to undo."
48
+ end
49
+
50
+ desc "followers LIST_NAME", "Remove all followers from a list."
51
+ def followers(list_name)
52
+ list_member_ids = collect_with_cursor do |cursor|
53
+ client.list_members(list_name, :cursor => cursor, :skip_status => true, :include_entities => false)
54
+ end
55
+ follower_ids = collect_with_cursor do |cursor|
56
+ client.follower_ids(:cursor => cursor)
57
+ end
58
+ list_member_ids_to_remove = (follower_ids - list_member_ids)
59
+ number = list_member_ids_to_remove.length
60
+ if number.zero?
61
+ return say "None of @#{@rcfile.default_profile[0]}'s followers are members of the list \"#{list_name}\"."
62
+ else
63
+ return unless yes? "Are you sure you want to remove #{number} #{number == 1 ? 'follower' : 'followers'} from the list \"#{list_name}\"?"
64
+ end
65
+ list_member_ids_to_remove.threaded_map do |list_member_id|
66
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
67
+ client.list_remove_member(list_name, list_member_id)
68
+ end
69
+ end
70
+ say "@#{@rcfile.default_profile[0]} removed #{number} #{number == 1 ? 'follower' : 'followers'} from the list \"#{list_name}\"."
71
+ say
72
+ say "Run `#{$0} list add all followers #{list_name}` to undo."
73
+ end
74
+
75
+ desc "listed FROM_LIST_NAME TO_LIST_NAME", "Remove all list members from a list."
76
+ def listed(from_list_name, to_list_name)
77
+ to_list_members = collect_with_cursor do |cursor|
78
+ client.list_members(to_list_name, :cursor => cursor, :skip_status => true, :include_entities => false)
79
+ end
80
+ from_list_members = collect_with_cursor do |cursor|
81
+ client.list_members(from_list_name, :cursor => cursor, :skip_status => true, :include_entities => false)
82
+ end
83
+ list_member_ids_to_remove = (from_list_members.collect(&:id) - to_list_members.collect(&:id))
84
+ number = list_member_ids_to_remove.length
85
+ if number.zero?
86
+ return say "None of the members of the list \"#{from_list_name}\" are members of the list \"#{to_list_name}\"."
87
+ else
88
+ return unless yes? "Are you sure you want to remove #{number} #{number == 1 ? 'member' : 'members'} from the list \"#{to_list_name}\"?"
89
+ end
90
+ list_member_ids_to_remove.threaded_map do |list_member_id|
91
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
92
+ client.list_remove_member(to_list_name, list_member_id)
93
+ end
94
+ end
95
+ say "@#{@rcfile.default_profile[0]} removed #{number} #{number == 1 ? 'member' : 'members'} from the list \"#{to_list_name}\"."
96
+ say
97
+ say "Run `#{$0} list add all listed #{from_list_name} #{to_list_name}` to undo."
98
+ end
99
+
100
+ desc "members LIST_NAME", "Remove all members from a list."
101
+ def members(list_name)
102
+ list_members = collect_with_cursor do |cursor|
103
+ client.list_members(list_name, :cursor => cursor, :skip_status => true, :include_entities => false)
104
+ end
105
+ number = list_members.length
106
+ return say "The list \"#{list_name}\" doesn't have any members." if number.zero?
107
+ return unless yes? "Are you sure you want to remove #{number} #{number == 1 ? 'member' : 'members'} from the list \"#{list_name}\"?"
108
+ list_members.collect(&:id).threaded_map do |list_member_id|
109
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
110
+ client.list_remove_member(list_name, list_member_id)
111
+ end
112
+ end
113
+ say "@#{@rcfile.default_profile[0]} removed #{number} #{number == 1 ? 'member' : 'members'} from the list \"#{list_name}\"."
114
+ end
115
+ map %w(all) => :members
116
+
21
117
  desc "users LIST_NAME SCREEN_NAME [SCREEN_NAME...]", "Remove users from a list."
22
118
  def users(list_name, screen_name, *screen_names)
23
119
  screen_names.unshift(screen_name)
24
120
  screen_names.threaded_map do |screen_name|
25
- screen_name = screen_name.strip_at
26
- client.list_remove_member(list_name, screen_name)
121
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
122
+ client.list_remove_member(list_name, screen_name)
123
+ end
27
124
  end
28
125
  number = screen_names.length
29
126
  say "@#{@rcfile.default_profile[0]} removed #{number} #{number == 1 ? 'user' : 'users'} from the list \"#{list_name}\"."
@@ -31,10 +128,6 @@ module T
31
128
  say "Run `#{$0} list add users #{list_name} #{screen_names.join(' ')}` to undo."
32
129
  end
33
130
 
34
- desc "all SUBCOMMAND ...ARGS", "Remove all users to a list."
35
- require 't/cli/list/remove/all'
36
- subcommand 'all', CLI::List::Remove::All
37
-
38
131
  private
39
132
 
40
133
  def base_url
@@ -0,0 +1,128 @@
1
+ require 'action_view'
2
+ require 'retryable'
3
+ require 't/core_ext/enumerable'
4
+ require 't/rcfile'
5
+ require 'thor'
6
+ require 'twitter'
7
+
8
+ module T
9
+ class CLI
10
+ class Search < Thor
11
+ include ActionView::Helpers::DateHelper
12
+
13
+ DEFAULT_HOST = 'api.twitter.com'
14
+ DEFAULT_PROTOCOL = 'https'
15
+ DEFAULT_NUM_RESULTS = 20
16
+ MAX_PAGES = 16
17
+ MAX_NUM_RESULTS = 200
18
+ MAX_SCREEN_NAME_SIZE = 20
19
+
20
+ check_unknown_options!
21
+
22
+ def initialize(*)
23
+ super
24
+ @rcfile = RCFile.instance
25
+ end
26
+
27
+ desc "all QUERY", "Returns the #{DEFAULT_NUM_RESULTS} most recent Tweets that match a specified query."
28
+ method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
29
+ method_option :reverse, :aliases => "-r", :type => :boolean, :default => false
30
+ def all(query)
31
+ defaults = {:include_entities => false}
32
+ defaults.merge!(:rpp => options['number']) if options['number']
33
+ timeline = client.search(query, defaults)
34
+ timeline.reverse! if options['reverse']
35
+ run_pager
36
+ timeline.each do |status|
37
+ say "#{status.from_user.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text} (#{time_ago_in_words(status.created_at)} ago)"
38
+ end
39
+ end
40
+
41
+ desc "timeline QUERY", "Returns Tweets in your timeline that match a specified query."
42
+ def timeline(query)
43
+ timeline = 1.upto(MAX_PAGES).threaded_map do |page|
44
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
45
+ client.home_timeline(:page => page, :count => MAX_NUM_RESULTS).map do |status|
46
+ status if /#{query}/i.match(status.text)
47
+ end
48
+ end
49
+ end
50
+ run_pager
51
+ timeline.flatten.compact.each do |status|
52
+ say "#{status.user.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text} (#{time_ago_in_words(status.created_at)} ago)"
53
+ end
54
+ end
55
+ map %w(tl) => :timeline
56
+
57
+ desc "user SCREEN_NAME QUERY", "Returns Tweets in a user's timeline that match a specified query."
58
+ def user(screen_name, query)
59
+ screen_name = screen_name.strip_at
60
+ timeline = 1.upto(MAX_PAGES).threaded_map do |page|
61
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
62
+ client.user_timeline(screen_name, :page => page, :count => MAX_NUM_RESULTS).map do |status|
63
+ status if /#{query}/i.match(status.text)
64
+ end
65
+ end
66
+ end
67
+ run_pager
68
+ timeline.flatten.compact.each do |status|
69
+ say "#{status.user.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text} (#{time_ago_in_words(status.created_at)} ago)"
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def base_url
76
+ "#{protocol}://#{host}"
77
+ end
78
+
79
+ def client
80
+ return @client if @client
81
+ @rcfile.path = parent_options['profile'] if parent_options['profile']
82
+ @client = Twitter::Client.new(
83
+ :endpoint => base_url,
84
+ :consumer_key => @rcfile.default_consumer_key,
85
+ :consumer_secret => @rcfile.default_consumer_secret,
86
+ :oauth_token => @rcfile.default_token,
87
+ :oauth_token_secret => @rcfile.default_secret
88
+ )
89
+ end
90
+
91
+ def host
92
+ parent_options['host'] || DEFAULT_HOST
93
+ end
94
+
95
+ def protocol
96
+ parent_options['no_ssl'] ? 'http' : DEFAULT_PROTOCOL
97
+ end
98
+
99
+ def run_pager
100
+ return if RUBY_PLATFORM =~ /win32/
101
+ return if ENV["T_ENV"] == "test"
102
+ return unless STDOUT.tty?
103
+
104
+ read, write = IO.pipe
105
+
106
+ unless Kernel.fork # Child process
107
+ STDOUT.reopen(write)
108
+ STDERR.reopen(write) if STDERR.tty?
109
+ read.close
110
+ write.close
111
+ return
112
+ end
113
+
114
+ # Parent process, become pager
115
+ STDIN.reopen(read)
116
+ read.close
117
+ write.close
118
+
119
+ ENV['LESS'] = 'FSRX' # Don't page if the input is short enough
120
+
121
+ Kernel.select [STDIN] # Wait until we have input before we start the pager
122
+ pager = ENV['PAGER'] || 'less'
123
+ exec pager rescue exec "/bin/sh", "-c", pager
124
+ end
125
+
126
+ end
127
+ end
128
+ end
data/lib/t/cli/set.rb CHANGED
@@ -26,7 +26,7 @@ module T
26
26
  def default(screen_name, consumer_key=nil)
27
27
  screen_name = screen_name.strip_at
28
28
  @rcfile.path = parent_options['profile'] if parent_options['profile']
29
- consumer_key = rcfile[screen_name].keys.last if consumer_key.nil?
29
+ consumer_key = @rcfile[screen_name].keys.last if consumer_key.nil?
30
30
  @rcfile.default_profile = {'username' => screen_name, 'consumer_key' => consumer_key}
31
31
  say "Default account has been updated."
32
32
  end
@@ -1,5 +1,7 @@
1
+ require 'retryable'
1
2
  require 't/core_ext/enumerable'
2
3
  require 't/core_ext/string'
4
+ require 't/collectable'
3
5
  require 't/rcfile'
4
6
  require 'thor'
5
7
  require 'twitter'
@@ -7,6 +9,8 @@ require 'twitter'
7
9
  module T
8
10
  class CLI
9
11
  class Unfollow < Thor
12
+ include T::Collectable
13
+
10
14
  DEFAULT_HOST = 'api.twitter.com'
11
15
  DEFAULT_PROTOCOL = 'https'
12
16
 
@@ -17,12 +21,96 @@ module T
17
21
  @rcfile = RCFile.instance
18
22
  end
19
23
 
24
+ desc "listed LIST_NAME", "Unfollow all members of a list."
25
+ def listed(list_name)
26
+ list_member_collection = collect_with_cursor do |cursor|
27
+ client.list_members(list_name, :cursor => cursor, :include_entities => false, :skip_status => true)
28
+ end
29
+ number = list_member_collection.length
30
+ return say "@#{@rcfile.default_profile[0]} is already not following any list members." if number.zero?
31
+ return unless yes? "Are you sure you want to unfollow #{number} #{number == 1 ? 'user' : 'users'}?"
32
+ list_member_collection.threaded_map do |list_member|
33
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
34
+ client.unfollow(list_member.id, :include_entities => false)
35
+ end
36
+ end
37
+ say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
38
+ say
39
+ say "Run `#{$0} follow all listed #{list_name}` to follow again."
40
+ end
41
+
42
+ desc "followers", "Unfollow all followers."
43
+ def followers
44
+ follower_ids = collect_with_cursor do |cursor|
45
+ client.follower_ids(:cursor => cursor)
46
+ end
47
+ friend_ids = collect_with_cursor do |cursor|
48
+ friends = client.friend_ids(:cursor => cursor)
49
+ end
50
+ follow_ids = (follower_ids - friend_ids)
51
+ number = follow_ids.length
52
+ return say "@#{@rcfile.default_profile[0]} is already not following any followers." if number.zero?
53
+ return unless yes? "Are you sure you want to unfollow #{number} #{number == 1 ? 'user' : 'users'}?"
54
+ screen_names = follow_ids.threaded_map do |follow_id|
55
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
56
+ client.unfollow(follow_id, :include_entities => false)
57
+ end
58
+ end
59
+ say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
60
+ say
61
+ say "Run `#{$0} follow all followers` to stop."
62
+ end
63
+
64
+ desc "friends", "Unfollow all friends."
65
+ def friends
66
+ friend_ids = collect_with_cursor do |cursor|
67
+ client.friend_ids(:cursor => cursor)
68
+ end
69
+ number = friend_ids.length
70
+ return say "@#{@rcfile.default_profile[0]} is already not following anyone." if number.zero?
71
+ return unless yes? "Are you sure you want to unfollow #{number} #{number == 1 ? 'user' : 'users'}?"
72
+ screen_names = friend_ids.threaded_map do |friend_id|
73
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
74
+ user = client.unfollow(friend_id, :include_entities => false)
75
+ user.screen_name
76
+ end
77
+ end
78
+ say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
79
+ say
80
+ say "Run `#{$0} follow users #{screen_names.join(' ')}` to follow again."
81
+ end
82
+ map %w(everyone everybody) => :friends
83
+
84
+ desc "nonfollowers", "Unfollow all non-followers."
85
+ def nonfollowers
86
+ friend_ids = collect_with_cursor do |cursor|
87
+ client.friend_ids(:cursor => cursor)
88
+ end
89
+ follower_ids = collect_with_cursor do |cursor|
90
+ client.follower_ids(:cursor => cursor)
91
+ end
92
+ unfollow_ids = (friend_ids - follower_ids)
93
+ number = unfollow_ids.length
94
+ return say "@#{@rcfile.default_profile[0]} is already not following any non-followers." if number.zero?
95
+ return unless yes? "Are you sure you want to unfollow #{number} #{number == 1 ? 'user' : 'users'}?"
96
+ screen_names = unfollow_ids.threaded_map do |unfollow_id|
97
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
98
+ user = client.unfollow(unfollow_id, :include_entities => false)
99
+ user.screen_name
100
+ end
101
+ end
102
+ say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
103
+ say
104
+ say "Run `#{$0} follow users #{screen_names.join(' ')}` to follow again."
105
+ end
106
+
20
107
  desc "users SCREEN_NAME [SCREEN_NAME...]", "Allows you to stop following users."
21
108
  def users(screen_name, *screen_names)
22
109
  screen_names.unshift(screen_name)
23
110
  screen_names.threaded_map do |screen_name|
24
- screen_name = screen_name.strip_at
25
- client.unfollow(screen_name, :include_entities => false)
111
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
112
+ client.unfollow(screen_name, :include_entities => false)
113
+ end
26
114
  end
27
115
  number = screen_names.length
28
116
  say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
@@ -30,10 +118,6 @@ module T
30
118
  say "Run `#{$0} follow users #{screen_names.join(' ')}` to follow again."
31
119
  end
32
120
 
33
- desc "all SUBCOMMAND ...ARGS", "Follow all users."
34
- require 't/cli/unfollow/all'
35
- subcommand 'all', CLI::Unfollow::All
36
-
37
121
  private
38
122
 
39
123
  def base_url
@@ -0,0 +1,12 @@
1
+ module T
2
+ module Collectable
3
+
4
+ def collect_with_cursor(collection=[], cursor=-1, &block)
5
+ return collection if cursor == 0
6
+ object = yield cursor
7
+ collection += object.collection
8
+ collect_with_cursor(collection, object.next_cursor)
9
+ end
10
+
11
+ end
12
+ end
data/lib/t/version.rb CHANGED
@@ -8,12 +8,12 @@ module T
8
8
 
9
9
  # @return [Integer]
10
10
  def self.minor
11
- 2
11
+ 3
12
12
  end
13
13
 
14
14
  # @return [Integer]
15
15
  def self.patch
16
- 1
16
+ 0
17
17
  end
18
18
 
19
19
  # @return [String, NilClass]
@@ -16,6 +16,181 @@ describe T::CLI::Follow do
16
16
  $stdout = @old_stdout
17
17
  end
18
18
 
19
+ describe "#followers" do
20
+ before do
21
+ @t.options = @t.options.merge(:profile => fixture_path + "/.trc")
22
+ end
23
+ context "no followers" do
24
+ before do
25
+ stub_get("/1/followers/ids.json").
26
+ with(:query => {:cursor => "-1"}).
27
+ to_return(:body => fixture("friends_ids.json"), :headers => {:content_type => "application/json; charset=utf-8"})
28
+ stub_get("/1/friends/ids.json").
29
+ with(:query => {:cursor => "-1"}).
30
+ to_return(:body => fixture("friends_ids.json"), :headers => {:content_type => "application/json; charset=utf-8"})
31
+ end
32
+ it "should request the correct resource" do
33
+ @t.follow("followers")
34
+ a_get("/1/followers/ids.json").
35
+ with(:query => {:cursor => "-1"}).
36
+ should have_been_made
37
+ a_get("/1/friends/ids.json").
38
+ with(:query => {:cursor => "-1"}).
39
+ should have_been_made
40
+ end
41
+ it "should have the correct output" do
42
+ @t.follow("followers")
43
+ $stdout.string.chomp.should == "@testcli is already following all followers."
44
+ end
45
+ end
46
+ context "one follower" do
47
+ before do
48
+ stub_get("/1/followers/ids.json").
49
+ with(:query => {:cursor => "-1"}).
50
+ to_return(:body => fixture("friends_ids.json"), :headers => {:content_type => "application/json; charset=utf-8"})
51
+ stub_get("/1/friends/ids.json").
52
+ with(:query => {:cursor => "-1"}).
53
+ to_return(:body => fixture("followers_ids.json"), :headers => {:content_type => "application/json; charset=utf-8"})
54
+ end
55
+ it "should request the correct resource" do
56
+ stub_post("/1/friendships/create.json").
57
+ with(:body => {:user_id => "7505382", :include_entities => "false"}).
58
+ to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
59
+ $stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
60
+ $stdin.should_receive(:gets).and_return("yes")
61
+ @t.follow("followers")
62
+ a_get("/1/followers/ids.json").
63
+ with(:query => {:cursor => "-1"}).
64
+ should have_been_made
65
+ a_get("/1/friends/ids.json").
66
+ with(:query => {:cursor => "-1"}).
67
+ should have_been_made
68
+ a_post("/1/friendships/create.json").
69
+ with(:body => {:user_id => "7505382", :include_entities => "false"}).
70
+ should have_been_made
71
+ end
72
+ context "yes" do
73
+ it "should have the correct output" do
74
+ stub_post("/1/friendships/create.json").
75
+ with(:body => {:user_id => "7505382", :include_entities => "false"}).
76
+ to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
77
+ $stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
78
+ $stdin.should_receive(:gets).and_return("yes")
79
+ @t.follow("followers")
80
+ $stdout.string.should =~ /^@testcli is now following 1 more user\.$/
81
+ end
82
+ end
83
+ context "no" do
84
+ it "should have the correct output" do
85
+ $stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
86
+ $stdin.should_receive(:gets).and_return("no")
87
+ @t.follow("followers")
88
+ $stdout.string.chomp.should == ""
89
+ end
90
+ end
91
+ context "Twitter is down" do
92
+ it "should retry 3 times and then raise an error" do
93
+ stub_post("/1/friendships/create.json").
94
+ with(:body => {:user_id => "7505382", :include_entities => "false"}).
95
+ to_return(:status => 502)
96
+ $stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
97
+ $stdin.should_receive(:gets).and_return("yes")
98
+ lambda do
99
+ @t.follow("followers")
100
+ end.should raise_error("Twitter is down or being upgraded.")
101
+ a_post("/1/friendships/create.json").
102
+ with(:body => {:user_id => "7505382", :include_entities => "false"}).
103
+ should have_been_made.times(3)
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ describe "#listed" do
110
+ before do
111
+ @t.options = @t.options.merge(:profile => fixture_path + "/.trc")
112
+ stub_get("/1/account/verify_credentials.json").
113
+ to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
114
+ end
115
+ context "no users" do
116
+ before do
117
+ stub_get("/1/lists/members.json").
118
+ with(:query => {:cursor => "-1", :include_entities => "false", :owner_screen_name => "sferik", :skip_status => "true", :slug => "presidents"}).
119
+ to_return(:body => fixture("empty_cursor.json"), :headers => {:content_type => "application/json; charset=utf-8"})
120
+ end
121
+ it "should request the correct resource" do
122
+ @t.follow("listed", "presidents")
123
+ a_get("/1/account/verify_credentials.json").
124
+ should have_been_made
125
+ a_get("/1/lists/members.json").
126
+ with(:query => {:cursor => "-1", :include_entities => "false", :owner_screen_name => "sferik", :skip_status => "true", :slug => "presidents"}).
127
+ should have_been_made
128
+ end
129
+ it "should have the correct output" do
130
+ @t.follow("listed", "presidents")
131
+ $stdout.string.chomp.should == "@testcli is already following all list members."
132
+ end
133
+ end
134
+ context "one user" do
135
+ before do
136
+ @t.options = @t.options.merge(:profile => fixture_path + "/.trc")
137
+ stub_get("/1/lists/members.json").
138
+ with(:query => {:cursor => "-1", :include_entities => "false", :owner_screen_name => "sferik", :skip_status => "true", :slug => "presidents"}).
139
+ to_return(:body => fixture("users_list.json"), :headers => {:content_type => "application/json; charset=utf-8"})
140
+ end
141
+ it "should request the correct resource" do
142
+ stub_post("/1/friendships/create.json").
143
+ with(:body => {:user_id => "7505382", :include_entities => "false"}).
144
+ to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
145
+ $stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
146
+ $stdin.should_receive(:gets).and_return("yes")
147
+ @t.follow("listed", "presidents")
148
+ a_get("/1/account/verify_credentials.json").
149
+ should have_been_made
150
+ a_get("/1/lists/members.json").
151
+ with(:query => {:cursor => "-1", :include_entities => "false", :owner_screen_name => "sferik", :skip_status => "true", :slug => "presidents"}).
152
+ should have_been_made
153
+ a_post("/1/friendships/create.json").
154
+ with(:body => {:user_id => "7505382", :include_entities => "false"}).
155
+ should have_been_made
156
+ end
157
+ context "yes" do
158
+ it "should have the correct output" do
159
+ stub_post("/1/friendships/create.json").
160
+ with(:body => {:user_id => "7505382", :include_entities => "false"}).
161
+ to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
162
+ $stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
163
+ $stdin.should_receive(:gets).and_return("yes")
164
+ @t.follow("listed", "presidents")
165
+ $stdout.string.should =~ /^@testcli is now following 1 more user\.$/
166
+ end
167
+ end
168
+ context "no" do
169
+ it "should have the correct output" do
170
+ $stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
171
+ $stdin.should_receive(:gets).and_return("no")
172
+ @t.follow("listed", "presidents")
173
+ $stdout.string.chomp.should == ""
174
+ end
175
+ end
176
+ context "Twitter is down" do
177
+ it "should retry 3 times and then raise an error" do
178
+ stub_post("/1/friendships/create.json").
179
+ with(:body => {:user_id => "7505382", :include_entities => "false"}).
180
+ to_return(:status => 502)
181
+ $stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
182
+ $stdin.should_receive(:gets).and_return("yes")
183
+ lambda do
184
+ @t.follow("listed", "presidents")
185
+ end.should raise_error("Twitter is down or being upgraded.")
186
+ a_post("/1/friendships/create.json").
187
+ with(:body => {:user_id => "7505382", :include_entities => "false"}).
188
+ should have_been_made.times(3)
189
+ end
190
+ end
191
+ end
192
+ end
193
+
19
194
  describe "#users" do
20
195
  before do
21
196
  @t.options = @t.options.merge(:profile => fixture_path + "/.trc")
@@ -28,21 +203,35 @@ describe T::CLI::Follow do
28
203
  end
29
204
  end
30
205
  context "one user" do
31
- before do
206
+ it "should request the correct resource" do
32
207
  stub_post("/1/friendships/create.json").
33
208
  with(:body => {:screen_name => "sferik", :include_entities => "false"}).
34
209
  to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
35
- end
36
- it "should request the correct resource" do
37
210
  @t.follow("users", "sferik")
38
211
  a_post("/1/friendships/create.json").
39
212
  with(:body => {:screen_name => "sferik", :include_entities => "false"}).
40
213
  should have_been_made
41
214
  end
42
215
  it "should have the correct output" do
216
+ stub_post("/1/friendships/create.json").
217
+ with(:body => {:screen_name => "sferik", :include_entities => "false"}).
218
+ to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
43
219
  @t.follow("users", "sferik")
44
220
  $stdout.string.should =~ /^@testcli is now following 1 more user\.$/
45
221
  end
222
+ context "Twitter is down" do
223
+ it "should retry 3 times and then raise an error" do
224
+ stub_post("/1/friendships/create.json").
225
+ with(:body => {:screen_name => "sferik", :include_entities => "false"}).
226
+ to_return(:status => 502)
227
+ lambda do
228
+ @t.follow("users", "sferik")
229
+ end.should raise_error("Twitter is down or being upgraded.")
230
+ a_post("/1/friendships/create.json").
231
+ with(:body => {:screen_name => "sferik", :include_entities => "false"}).
232
+ should have_been_made.times(3)
233
+ end
234
+ end
46
235
  end
47
236
  end
48
237