diff --git a/Rakefile b/Rakefile index afe2bc1f..36e6072e 100644 --- a/Rakefile +++ b/Rakefile @@ -19,6 +19,7 @@ desc "Build documentation" task :doc => [ :rdoc ] Rake::TestTask.new do |t| + t.libs << "test" t.test_files = Dir["test/**/*_test.rb"] t.verbose = true end diff --git a/lib/capistrano/configuration/actions/file_transfer.rb b/lib/capistrano/configuration/actions/file_transfer.rb index 72dc4662..c669122f 100644 --- a/lib/capistrano/configuration/actions/file_transfer.rb +++ b/lib/capistrano/configuration/actions/file_transfer.rb @@ -9,7 +9,9 @@ module Capistrano # by the current task. If :mode is specified it is used to # set the mode on the file. def put(data, path, options={}) - upload(StringIO.new(data), path, options) + opts = options.dup + opts[:permissions] = opts.delete(:mode) + upload(StringIO.new(data), path, opts) end # Get file remote_path from FIRST server targeted by diff --git a/lib/capistrano/configuration/connections.rb b/lib/capistrano/configuration/connections.rb index 27e67464..3d263452 100644 --- a/lib/capistrano/configuration/connections.rb +++ b/lib/capistrano/configuration/connections.rb @@ -1,6 +1,7 @@ require 'enumerator' require 'net/ssh/gateway' require 'capistrano/ssh' +require 'capistrano/errors' module Capistrano class Configuration @@ -24,8 +25,11 @@ module Capistrano def initialize(gateway, options) Thread.abort_on_exception = true server = ServerDefinition.new(gateway) - @gateway = Net::SSH::Gateway.new(server.host, server.user || ServerDefinition.default_user, server.options) + @options = options + @gateway = SSH.connection_strategy(server, options) do |host, user, connect_options| + Net::SSH::Gateway.new(host, user, connect_options) + end end def connect_to(server) @@ -177,8 +181,6 @@ module Capistrano def safely_establish_connection_to(server, failures=nil) sessions[server] ||= connection_factory.connect_to(server) rescue Exception => err -puts err -puts err.backtrace raise unless failures failures << { :server => server, :error => err } end diff --git a/lib/capistrano/ssh.rb b/lib/capistrano/ssh.rb index 47eaa2f1..7f577d55 100644 --- a/lib/capistrano/ssh.rb +++ b/lib/capistrano/ssh.rb @@ -39,7 +39,22 @@ module Capistrano # constructor. Values in +options+ are then merged into it, and any # connection information in +server+ is added last, so that +server+ info # takes precedence over +options+, which takes precendence over ssh_options. - def self.connect(server, options={}, &block) + def self.connect(server, options={}) + connection_strategy(server, options) do |host, user, connection_options| + connection = Net::SSH.start(host, user, connection_options) + Server.apply_to(connection, server) + end + end + + # Abstracts the logic for establishing an SSH connection (which includes + # testing for connection failures and retrying with a password, and so forth, + # mostly made complicated because of the fact that some of these variables + # might be lazily evaluated and try to do something like prompt the user, + # which should only happen when absolutely necessary. + # + # This will yield the hostname, username, and a hash of connection options + # to the given block, which should return a new connection. + def self.connection_strategy(server, options={}, &block) methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ] password_value = nil @@ -47,15 +62,15 @@ module Capistrano user = server.user || options[:user] || ssh_options[:username] || ServerDefinition.default_user ssh_options[:port] = server.port || options[:port] || ssh_options[:port] || DEFAULT_PORT + ssh_options.delete(:username) + begin connection_options = ssh_options.merge( :password => password_value, :auth_methods => ssh_options[:auth_methods] || methods.shift ) - connection = Net::SSH.start(server.host, user, connection_options, &block) - Server.apply_to(connection, server) - + yield server.host, user, connection_options rescue Net::SSH::AuthenticationFailed raise if methods.empty? || ssh_options[:auth_methods] password_value = options[:password] diff --git a/lib/capistrano/transfer.rb b/lib/capistrano/transfer.rb index ff14758c..88404475 100644 --- a/lib/capistrano/transfer.rb +++ b/lib/capistrano/transfer.rb @@ -31,9 +31,11 @@ module Capistrano @options = options @callback = callback - @transport = options.fetch(:transport, :sftp) + @transport = options.fetch(:via, :sftp) @logger = options.delete(:logger) - + + @session_map = {} + prepare_transfers end @@ -91,16 +93,18 @@ module Capistrano private - def prepare_transfers - @session_map = {} + def session_map + @session_map + end + def prepare_transfers logger.info "#{transport} #{operation} #{from} -> #{to}" if logger @transfers = sessions.map do |session| session_from = normalize(from, session) session_to = normalize(to, session) - @session_map[session] = case transport + session_map[session] = case transport when :sftp prepare_sftp_transfer(session_from, session_to, session) when :scp @@ -112,24 +116,21 @@ module Capistrano end def prepare_scp_transfer(from, to, session) - scp = Net::SCP.new(session) - real_callback = callback || Proc.new do |channel, name, sent, total| logger.trace "[#{channel[:host]}] #{name}" if logger && sent == 0 end channel = case direction when :up - scp.upload(from, to, options, &real_callback) + session.scp.upload(from, to, options, &real_callback) when :down - scp.download(from, to, options, &real_callback) + session.scp.download(from, to, options, &real_callback) else raise ArgumentError, "unsupported transfer direction: #{direction.inspect}" end - channel[:server] = session.xserver - channel[:host] = session.xserver.host - channel[:channel] = channel + channel[:server] = session.xserver + channel[:host] = session.xserver.host return channel end @@ -138,7 +139,7 @@ module Capistrano attr_reader :operation def initialize(session, &callback) - Net::SFTP::Session.new(session) do |sftp| + session.sftp(false).connect do |sftp| @operation = callback.call(sftp) end end @@ -170,15 +171,12 @@ module Capistrano elsif event == :finish logger.trace "[#{op[:host]}] done" end - - op[:channel].close if event == :finish end opts = options.dup opts[:properties] = (opts[:properties] || {}).merge( :server => session.xserver, - :host => session.xserver.host, - :channel => sftp.channel) + :host => session.xserver.host) case direction when :up @@ -205,12 +203,14 @@ module Capistrano end def handle_error(error) - transfer = @session_map[error.session] - transfer[:channel].close + transfer = session_map[error.session] transfer[:error] = error transfer[:failed] = true - transfer.abort! if transport == :sftp + case transport + when :sftp then transfer.abort! + when :scp then transfer.close + end end end end \ No newline at end of file diff --git a/test/command_test.rb b/test/command_test.rb index 67ded656..655ee73c 100644 --- a/test/command_test.rb +++ b/test/command_test.rb @@ -20,8 +20,7 @@ class CommandTest < Test::Unit::TestCase end def test_command_with_pty_should_request_pty_and_register_success_callback - session = setup_for_extracting_channel_action(:on_success) do |ch| - ch.expects(:request_pty).with(:want_reply => true) + session = setup_for_extracting_channel_action(:request_pty, true) do |ch| ch.expects(:exec).with(%(sh -c "ls")) end Capistrano::Command.new("ls", [session], :pty => true) @@ -128,7 +127,7 @@ class CommandTest < Test::Unit::TestCase end def test_unsuccessful_pty_request_should_close_channel - session = setup_for_extracting_channel_action(:on_failure) do |ch| + session = setup_for_extracting_channel_action(:request_pty, false) do |ch| ch.expects(:close) end Capistrano::Command.new("ls", [session], :pty => true) @@ -158,7 +157,7 @@ class CommandTest < Test::Unit::TestCase def test_on_request_should_record_exit_status data = mock(:read_long => 5) - session = setup_for_extracting_channel_action(:on_request, "exit-status", nil, data) do |ch| + session = setup_for_extracting_channel_action([:on_request, "exit-status"], data) do |ch| ch.expects(:[]=).with(:status, 5) end Capistrano::Command.new("ls", [session]) @@ -172,34 +171,34 @@ class CommandTest < Test::Unit::TestCase end def test_stop_should_close_all_open_channels - sessions = [mock("session", :open_channel => new_channel(false)), - mock("session", :open_channel => new_channel(true)), - mock("session", :open_channel => new_channel(false))] + sessions = [mock_session(new_channel(false)), + mock_session(new_channel(true)), + mock_session(new_channel(false))] cmd = Capistrano::Command.new("ls", sessions) cmd.stop! end def test_process_should_return_cleanly_if_all_channels_have_zero_exit_status - sessions = [mock("session", :open_channel => new_channel(true, 0)), - mock("session", :open_channel => new_channel(true, 0)), - mock("session", :open_channel => new_channel(true, 0))] + sessions = [mock_session(new_channel(true, 0)), + mock_session(new_channel(true, 0)), + mock_session(new_channel(true, 0))] cmd = Capistrano::Command.new("ls", sessions) assert_nothing_raised { cmd.process! } end def test_process_should_raise_error_if_any_channel_has_non_zero_exit_status - sessions = [mock("session", :open_channel => new_channel(true, 0)), - mock("session", :open_channel => new_channel(true, 0)), - mock("session", :open_channel => new_channel(true, 1))] + sessions = [mock_session(new_channel(true, 0)), + mock_session(new_channel(true, 0)), + mock_session(new_channel(true, 1))] cmd = Capistrano::Command.new("ls", sessions) assert_raises(Capistrano::CommandError) { cmd.process! } end def test_command_error_should_include_accessor_with_host_array - sessions = [mock("session", :open_channel => new_channel(true, 0)), - mock("session", :open_channel => new_channel(true, 0)), - mock("session", :open_channel => new_channel(true, 1))] + sessions = [mock_session(new_channel(true, 0)), + mock_session(new_channel(true, 0)), + mock_session(new_channel(true, 1))] cmd = Capistrano::Command.new("ls", sessions) begin @@ -216,43 +215,13 @@ class CommandTest < Test::Unit::TestCase ch = mock("channel") returns = [false] * (times-1) ch.stubs(:[]).with(:closed).returns(*(returns + [true])) - con = mock("connection") - con.expects(:process).with(true).times(times-1) - ch.expects(:connection).times(times-1).returns(con) ch.expects(:[]).with(:status).returns(0) ch end - sessions = [mock("session", :open_channel => new_channel[5]), - mock("session", :open_channel => new_channel[10]), - mock("session", :open_channel => new_channel[7])] - cmd = Capistrano::Command.new("ls", sessions) - assert_nothing_raised { cmd.process! } - end - - def test_process_should_ping_all_connections_each_second - now = Time.now - - new_channel = Proc.new do - ch = mock("channel") - ch.stubs(:now => now) - def ch.[](key) - case key - when :status then 0 - when :closed then Time.now - now < 1.1 ? false : true - else raise "unknown key: #{key}" - end - end - con = mock("connection") - con.stubs(:process) - con.expects(:ping!) - ch.stubs(:connection).returns(con) - ch - end - - sessions = [mock("session", :open_channel => new_channel[]), - mock("session", :open_channel => new_channel[]), - mock("session", :open_channel => new_channel[])] + sessions = [mock_session(new_channel[5]), + mock_session(new_channel[10]), + mock_session(new_channel[7])] cmd = Capistrano::Command.new("ls", sessions) assert_nothing_raised { cmd.process! } end @@ -281,6 +250,13 @@ class CommandTest < Test::Unit::TestCase private + def mock_session(channel=nil) + stub('session', :open_channel => channel, + :preprocess => true, + :postprocess => true, + :listeners => {}) + end + def new_channel(closed, status=nil) ch = mock("channel") ch.expects(:[]).with(:closed).returns(closed) @@ -300,7 +276,11 @@ class CommandTest < Test::Unit::TestCase channel.stubs(:[]).with(:server).returns(s) channel.stubs(:[]).with(:host).returns(s.host) - channel.expects(action).yields(channel, *args) if action + + if action + action = Array(action) + channel.expects(action.first).with(*action[1..-1]).yields(channel, *args) + end yield channel if block_given? diff --git a/test/configuration/actions/file_transfer_test.rb b/test/configuration/actions/file_transfer_test.rb index a993fa12..a7130760 100644 --- a/test/configuration/actions/file_transfer_test.rb +++ b/test/configuration/actions/file_transfer_test.rb @@ -4,6 +4,7 @@ require 'capistrano/configuration/actions/file_transfer' class ConfigurationActionsFileTransferTest < Test::Unit::TestCase class MockConfig include Capistrano::Configuration::Actions::FileTransfer + attr_accessor :sessions end def setup @@ -11,30 +12,31 @@ class ConfigurationActionsFileTransferTest < Test::Unit::TestCase @config.stubs(:logger).returns(stub_everything) end - def test_put_should_pass_options_to_execute_on_servers - @config.expects(:execute_on_servers).with(:foo => "bar") - @config.put("some data", "test.txt", :foo => "bar") - end - - def test_put_should_delegate_to_Upload_process - @config.expects(:execute_on_servers).yields(%w(s1 s2 s3).map { |s| mock(:host => s) }) - @config.expects(:sessions).times(3).returns(Hash.new{|h,k| h[k] = k.host.to_sym}) - Capistrano::Upload.expects(:process).with([:s1,:s2,:s3], "test.txt", :data => "some data", :mode => 0777, :logger => @config.logger) + def test_put_should_delegate_to_upload + @config.expects(:upload).with { |from, to, opts| + from.string == "some data" && to == "test.txt" && opts == { :permissions => 0777 } } @config.put("some data", "test.txt", :mode => 0777) end - def test_get_should_pass_options_execute_on_servers_including_once - @config.expects(:execute_on_servers).with(:foo => "bar", :once => true) - @config.get("test.txt", "test.txt", :foo => "bar") + def test_get_should_delegate_to_download_with_once + @config.expects(:download).with("testr.txt", "testl.txt", :foo => "bar", :once => true) + @config.get("testr.txt", "testl.txt", :foo => "bar") end - def test_get_should_use_sftp_get_file_to_local_path - sftp = mock("sftp", :state => :closed, :connect => true) - sftp.expects(:get_file).with("remote.txt", "local.txt") + def test_upload_should_delegate_to_transfer + @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", :foo => "bar") + @config.upload("testl.txt", "testr.txt", :foo => "bar") + end - s = server("capistrano") - @config.expects(:execute_on_servers).yields([s]) - @config.expects(:sessions).returns(s => mock("session", :sftp => sftp)) - @config.get("remote.txt", "local.txt") + def test_download_should_delegate_to_transfer + @config.expects(:transfer).with(:down, "testr.txt", "testl.txt", :foo => "bar") + @config.download("testr.txt", "testl.txt", :foo => "bar") + end + + def test_transfer_should_invoke_transfer_on_matching_servers + @config.sessions = { :a => 1, :b => 2, :c => 3, :d => 4 } + @config.expects(:execute_on_servers).with(:foo => "bar").yields([:a, :b, :c]) + Capistrano::Transfer.expects(:process).with(:up, "testl.txt", "testr.txt", [1,2,3], {:foo => "bar", :logger => @config.logger}) + @config.transfer(:up, "testl.txt", "testr.txt", :foo => "bar") end end \ No newline at end of file diff --git a/test/configuration/connections_test.rb b/test/configuration/connections_test.rb index 67aa6ba7..c9396e11 100644 --- a/test/configuration/connections_test.rb +++ b/test/configuration/connections_test.rb @@ -31,7 +31,7 @@ class ConfigurationConnectionsTest < Test::Unit::TestCase @config = MockConfig.new @config.stubs(:logger).returns(stub_everything) @ssh_options = { - :user => "jamis", + :user => "user", :port => 8080, :password => "g00b3r", :ssh_options => { :debug => :verbose } @@ -59,17 +59,16 @@ class ConfigurationConnectionsTest < Test::Unit::TestCase end def test_connection_factory_should_return_gateway_instance_if_gateway_variable_is_set - @config.values[:gateway] = "capistrano" - server = server("capistrano") - Capistrano::SSH.expects(:connect).with { |s,| s.host == "capistrano" }.yields(stub_everything) - assert_instance_of Capistrano::Gateway, @config.connection_factory + @config.values[:gateway] = "j@capistrano" + Net::SSH::Gateway.expects(:new).with("capistrano", "j", :port => 22, :password => nil, :auth_methods => %w(publickey hostbased)).returns(stub_everything) + assert_instance_of Capistrano::Configuration::Connections::GatewayConnectionFactory, @config.connection_factory end def test_connection_factory_as_gateway_should_honor_config_options @config.values[:gateway] = "capistrano" @config.values.update(@ssh_options) - Capistrano::SSH.expects(:connect).with { |s,opts| s.host == "capistrano" && opts == @config }.yields(stub_everything) - assert_instance_of Capistrano::Gateway, @config.connection_factory + Net::SSH::Gateway.expects(:new).with("capistrano", "user", :debug => :verbose, :port => 8080, :password => nil, :auth_methods => %w(publickey hostbased)).returns(stub_everything) + assert_instance_of Capistrano::Configuration::Connections::GatewayConnectionFactory, @config.connection_factory end def test_establish_connections_to_should_accept_a_single_nonarray_parameter @@ -194,11 +193,11 @@ class ConfigurationConnectionsTest < Test::Unit::TestCase @config.current_task = mock_task @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([server("cap1")]) Capistrano::SSH.expects(:connect).raises(Exception) - assert_raises(Capistrano::ConnectionError) { + assert_raises(Capistrano::ConnectionError) do @config.execute_on_servers do flunk "expected an exception to be raised" end - } + end end def test_execute_servers_should_not_raise_connection_error_on_failure_with_on_errors_continue @@ -243,14 +242,14 @@ class ConfigurationConnectionsTest < Test::Unit::TestCase end end - def test_execute_on_servers_should_not_try_to_connect_to_hosts_with_upload_errors_with_on_errors_continue + def test_execute_on_servers_should_not_try_to_connect_to_hosts_with_transfer_errors_with_on_errors_continue cap1 = server("cap1") cap2 = server("cap2") @config.current_task = mock_task(:on_error => :continue) @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([cap1, cap2]) Capistrano::SSH.expects(:connect).times(2).returns(:success) @config.execute_on_servers do |servers| - error = Capistrano::UploadError.new + error = Capistrano::TransferError.new error.hosts = [cap1] raise error end diff --git a/test/ssh_test.rb b/test/ssh_test.rb index f501f441..f0fe5953 100644 --- a/test/ssh_test.rb +++ b/test/ssh_test.rb @@ -3,93 +3,93 @@ require 'capistrano/ssh' class SSHTest < Test::Unit::TestCase def setup - @options = { :username => nil, - :password => nil, + Capistrano::ServerDefinition.stubs(:default_user).returns("default-user") + @options = { :password => nil, :port => 22, :auth_methods => %w(publickey hostbased) } @server = server("capistrano") end def test_connect_with_bare_server_without_options_or_config_with_public_key_succeeding_should_only_loop_once - Net::SSH.expects(:start).with(@server.host, @options).returns(success = Object.new) + Net::SSH.expects(:start).with(@server.host, "default-user", @options).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(@server) end def test_connect_with_bare_server_without_options_with_public_key_failing_should_try_password - Net::SSH.expects(:start).with(@server.host, @options).raises(Net::SSH::AuthenticationFailed) - Net::SSH.expects(:start).with(@server.host, @options.merge(:password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).returns(success = Object.new) + Net::SSH.expects(:start).with(@server.host, "default-user", @options).raises(Net::SSH::AuthenticationFailed) + Net::SSH.expects(:start).with(@server.host, "default-user", @options.merge(:password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(@server, :password => "f4b13n") end def test_connect_with_bare_server_without_options_public_key_and_password_failing_should_raise_error - Net::SSH.expects(:start).with(@server.host, @options).raises(Net::SSH::AuthenticationFailed) - Net::SSH.expects(:start).with(@server.host, @options.merge(:password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).raises(Net::SSH::AuthenticationFailed) + Net::SSH.expects(:start).with(@server.host, "default-user", @options).raises(Net::SSH::AuthenticationFailed) + Net::SSH.expects(:start).with(@server.host, "default-user", @options.merge(:password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).raises(Net::SSH::AuthenticationFailed) assert_raises(Net::SSH::AuthenticationFailed) do Capistrano::SSH.connect(@server, :password => "f4b13n") end end def test_connect_with_bare_server_and_user_via_public_key_should_pass_user_to_net_ssh - Net::SSH.expects(:start).with(@server.host, @options.merge(:username => "jamis")).returns(success = Object.new) + Net::SSH.expects(:start).with(@server.host, "jamis", @options).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(@server, :user => "jamis") end def test_connect_with_bare_server_and_user_via_password_should_pass_user_to_net_ssh - Net::SSH.expects(:start).with(@server.host, @options.merge(:username => "jamis")).raises(Net::SSH::AuthenticationFailed) - Net::SSH.expects(:start).with(@server.host, @options.merge(:username => "jamis", :password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).returns(success = Object.new) + Net::SSH.expects(:start).with(@server.host, "jamis", @options).raises(Net::SSH::AuthenticationFailed) + Net::SSH.expects(:start).with(@server.host, "jamis", @options.merge(:password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(@server, :user => "jamis", :password => "f4b13n") end def test_connect_with_bare_server_with_explicit_port_should_pass_port_to_net_ssh - Net::SSH.expects(:start).with(@server.host, @options.merge(:port => 1234)).returns(success = Object.new) + Net::SSH.expects(:start).with(@server.host, "default-user", @options.merge(:port => 1234)).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(@server, :port => 1234) end def test_connect_with_server_with_user_should_pass_user_to_net_ssh server = server("jamis@capistrano") - Net::SSH.expects(:start).with(server.host, @options.merge(:username => "jamis")).returns(success = Object.new) + Net::SSH.expects(:start).with(server.host, "jamis", @options).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(server) end def test_connect_with_server_with_port_should_pass_port_to_net_ssh server = server("capistrano:1235") - Net::SSH.expects(:start).with(server.host, @options.merge(:port => 1235)).returns(success = Object.new) + Net::SSH.expects(:start).with(server.host, "default-user", @options.merge(:port => 1235)).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(server) end def test_connect_with_server_with_user_and_port_should_pass_user_and_port_to_net_ssh server = server("jamis@capistrano:1235") - Net::SSH.expects(:start).with(server.host, @options.merge(:username => "jamis", :port => 1235)).returns(success = Object.new) + Net::SSH.expects(:start).with(server.host, "jamis", @options.merge(:port => 1235)).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(server) end def test_connect_with_server_with_other_ssh_options_should_pass_ssh_options_to_net_ssh server = server("jamis@capistrano:1235", :ssh_options => { :keys => %w(some_valid_key), :auth_methods => %w(a_method), :hmac => 'none' }) - Net::SSH.expects(:start).with(server.host, @options.merge(:username => "jamis", :port => 1235, :keys => %w(some_valid_key), :auth_methods => %w(a_method), :hmac => 'none' )).returns(success = Object.new) + Net::SSH.expects(:start).with(server.host, "jamis", @options.merge(:port => 1235, :keys => %w(some_valid_key), :auth_methods => %w(a_method), :hmac => 'none' )).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(server) end def test_connect_with_ssh_options_should_use_ssh_options ssh_options = { :username => "JamisMan", :port => 8125 } - Net::SSH.expects(:start).with(@server.host, @options.merge(:username => "JamisMan", :port => 8125)).returns(success = Object.new) + Net::SSH.expects(:start).with(@server.host, "JamisMan", @options.merge(:port => 8125)).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(@server, {:ssh_options => ssh_options}) end def test_connect_with_options_and_ssh_options_should_see_options_override_ssh_options ssh_options = { :username => "JamisMan", :port => 8125, :forward_agent => true } - Net::SSH.expects(:start).with(@server.host, @options.merge(:username => "jamis", :port => 1235, :forward_agent => true)).returns(success = Object.new) - assert_equal success, Capistrano::SSH.connect(@server, {:ssh_options => ssh_options, :user => "jamis", :port => 1235}) + Net::SSH.expects(:start).with(@server.host, "jamis", @options.merge(:port => 1235, :forward_agent => true)).returns(success = Object.new) + assert_equal success, Capistrano::SSH.connect(@server, :ssh_options => ssh_options, :user => "jamis", :port => 1235) end def test_connect_with_ssh_options_should_see_server_options_override_ssh_options ssh_options = { :username => "JamisMan", :port => 8125, :forward_agent => true } server = server("jamis@capistrano:1235") - Net::SSH.expects(:start).with(server.host, @options.merge(:username => "jamis", :port => 1235, :forward_agent => true)).returns(success = Object.new) + Net::SSH.expects(:start).with(server.host, "jamis", @options.merge(:port => 1235, :forward_agent => true)).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(server, {:ssh_options => ssh_options}) end def test_connect_should_add_xserver_accessor_to_connection - Net::SSH.expects(:start).with(@server.host, @options).returns(success = Object.new) + Net::SSH.expects(:start).with(@server.host, "default-user", @options).returns(success = Object.new) assert_equal success, Capistrano::SSH.connect(@server) assert success.respond_to?(:xserver) assert success.respond_to?(:xserver) @@ -97,7 +97,7 @@ class SSHTest < Test::Unit::TestCase end def test_connect_should_not_retry_if_custom_auth_methods_are_given - Net::SSH.expects(:start).with(@server.host, @options.merge(:auth_methods => %w(publickey))).raises(Net::SSH::AuthenticationFailed) + Net::SSH.expects(:start).with(@server.host, "default-user", @options.merge(:auth_methods => %w(publickey))).raises(Net::SSH::AuthenticationFailed) assert_raises(Net::SSH::AuthenticationFailed) { Capistrano::SSH.connect(@server, :ssh_options => { :auth_methods => %w(publickey) }) } end end diff --git a/test/transfer_test.rb b/test/transfer_test.rb new file mode 100644 index 00000000..f98c44b2 --- /dev/null +++ b/test/transfer_test.rb @@ -0,0 +1,160 @@ +require 'utils' +require 'capistrano/transfer' + +class TransferTest < Test::Unit::TestCase + def test_class_process_should_delegate_to_instance_process + Capistrano::Transfer.expects(:new).with(:up, "from", "to", %w(a b c), {}).returns(mock('transfer', :process! => nil)).yields + yielded = false + Capistrano::Transfer.process(:up, "from", "to", %w(a b c), {}) { yielded = true } + assert yielded + end + + def test_default_transport_is_sftp + transfer = Capistrano::Transfer.new(:up, "from", "to", []) + assert_equal :sftp, transfer.transport + end + + def test_active_is_true_when_any_sftp_transfers_are_active + returns = [false, false, true] + sessions = [session('app1', :sftp), session('app2', :sftp), session('app3', :sftp)].each { |s| s.xsftp.expects(:upload).returns(stub('operation', :active? => returns.shift)) } + transfer = Capistrano::Transfer.new(:up, "from", "to", sessions, :via => :sftp) + assert_equal true, transfer.active? + end + + def test_active_is_false_when_all_sftp_transfers_are_not_active + sessions = [session('app1', :sftp), session('app2', :sftp)].each { |s| s.xsftp.expects(:upload).returns(stub('operation', :active? => false)) } + transfer = Capistrano::Transfer.new(:up, "from", "to", sessions, :via => :sftp) + assert_equal false, transfer.active? + end + + def test_active_is_true_when_any_scp_transfers_are_active + returns = [false, false, true] + sessions = [session('app1', :scp), session('app2', :scp), session('app3', :scp)].each do |s| + channel = stub('channel', :[]= => nil, :active? => returns.shift) + s.scp.expects(:upload).returns(channel) + end + transfer = Capistrano::Transfer.new(:up, "from", "to", sessions, :via => :scp) + assert_equal true, transfer.active? + end + + def test_active_is_false_when_all_scp_transfers_are_not_active + sessions = [session('app1', :scp), session('app2', :scp), session('app3', :scp)].each do |s| + channel = stub('channel', :[]= => nil, :active? => false) + s.scp.expects(:upload).returns(channel) + end + transfer = Capistrano::Transfer.new(:up, "from", "to", sessions, :via => :scp) + assert_equal false, transfer.active? + end + + [:up, :down].each do |direction| + define_method("test_sftp_#{direction}load_from_file_to_file_should_normalize_from_and_to") do + sessions = [session('app1', :sftp), session('app2', :sftp)] + + sessions.each do |session| + session.xsftp.expects("#{direction}load".to_sym).with("from-#{session.xserver.host}", "to-#{session.xserver.host}", + :properties => { :server => session.xserver, :host => session.xserver.host }) + end + + transfer = Capistrano::Transfer.new(direction, "from-$CAPISTRANO:HOST$", "to-$CAPISTRANO:HOST$", sessions) + end + + define_method("test_scp_#{direction}load_from_file_to_file_should_normalize_from_and_to") do + sessions = [session('app1', :scp), session('app2', :scp)] + + sessions.each do |session| + session.scp.expects("#{direction}load".to_sym).returns({}).with("from-#{session.xserver.host}", "to-#{session.xserver.host}", :via => :scp) + end + + transfer = Capistrano::Transfer.new(direction, "from-$CAPISTRANO:HOST$", "to-$CAPISTRANO:HOST$", sessions, :via => :scp) + end + end + + def test_sftp_upload_from_IO_to_file_should_clone_the_IO_for_each_connection + sessions = [session('app1', :sftp), session('app2', :sftp)] + io = StringIO.new("from here") + + sessions.each do |session| + session.xsftp.expects(:upload).with do |from, to, opts| + from != io && from.is_a?(StringIO) && from.string == io.string && + to == "/to/here-#{session.xserver.host}" && + opts[:properties][:server] == session.xserver && + opts[:properties][:host] == session.xserver.host + end + end + + transfer = Capistrano::Transfer.new(:up, StringIO.new("from here"), "/to/here-$CAPISTRANO:HOST$", sessions) + end + + def test_scp_upload_from_IO_to_file_should_clone_the_IO_for_each_connection + sessions = [session('app1', :scp), session('app2', :scp)] + io = StringIO.new("from here") + + sessions.each do |session| + channel = mock('channel') + channel.expects(:[]=).with(:server, session.xserver) + channel.expects(:[]=).with(:host, session.xserver.host) + session.scp.expects(:upload).returns(channel).with do |from, to, opts| + from != io && from.is_a?(StringIO) && from.string == io.string && + to == "/to/here-#{session.xserver.host}" + end + end + + transfer = Capistrano::Transfer.new(:up, StringIO.new("from here"), "/to/here-$CAPISTRANO:HOST$", sessions, :via => :scp) + end + + def test_process_should_block_until_transfer_is_no_longer_active + transfer = Capistrano::Transfer.new(:up, "from", "to", []) + transfer.expects(:process_iteration).times(4).yields.returns(true,true,true,false) + transfer.expects(:active?).times(4) + transfer.process! + end + + def test_errors_raised_for_a_sftp_session_should_abort_session_and_continue_with_remaining_sessions + s = session('app1') + error = ExceptionWithSession.new(s) + transfer = Capistrano::Transfer.new(:up, "from", "to", []) + transfer.expects(:process_iteration).raises(error).times(3).returns(true, false) + txfr = mock('transfer', :abort! => true) + txfr.expects(:[]=).with(:failed, true) + txfr.expects(:[]=).with(:error, error) + transfer.expects(:session_map).returns(s => txfr) + transfer.process! + end + + def test_errors_raised_for_a_scp_session_should_abort_session_and_continue_with_remaining_sessions + s = session('app1') + error = ExceptionWithSession.new(s) + transfer = Capistrano::Transfer.new(:up, "from", "to", [], :via => :scp) + transfer.expects(:process_iteration).raises(error).times(3).returns(true, false) + txfr = mock('channel', :close => true) + txfr.expects(:[]=).with(:failed, true) + txfr.expects(:[]=).with(:error, error) + transfer.expects(:session_map).returns(s => txfr) + transfer.process! + end + + private + + class ExceptionWithSession < ::Exception + attr_reader :session + + def initialize(session) + @session = session + super() + end + end + + def session(host, mode=nil) + session = stub('session', :xserver => stub('server', :host => host)) + case mode + when :sftp + sftp = stub('sftp') + session.expects(:sftp).with(false).returns(sftp) + sftp.expects(:connect).yields(sftp).returns(sftp) + session.stubs(:xsftp).returns(sftp) + when :scp + session.stubs(:scp).returns(stub('scp')) + end + session + end +end \ No newline at end of file diff --git a/test/upload_test.rb b/test/upload_test.rb deleted file mode 100644 index d6aeb0b4..00000000 --- a/test/upload_test.rb +++ /dev/null @@ -1,131 +0,0 @@ -require "utils" -require 'capistrano/upload' - -class UploadTest < Test::Unit::TestCase - def setup - @mode = IO::WRONLY | IO::CREAT | IO::TRUNC - end - - def test_initialize_should_raise_error_if_data_is_missing - assert_raises(ArgumentError) do - Capistrano::Upload.new([], "test.txt", :foo => "bar") - end - end - - def test_initialize_should_get_sftp_for_each_session - new_sftp = Proc.new do |state| - sftp = mock("sftp", :state => state, :open => nil) - sftp.expects(:connect) unless state == :open - sftp.stubs(:channel).returns({}) - sftp - end - - sessions = [mock("session", :xserver => server("a"), :sftp => new_sftp[:closed]), - mock("session", :xserver => server("b"), :sftp => new_sftp[:closed]), - mock("session", :xserver => server("c"), :sftp => new_sftp[:open])] - Capistrano::Upload.new(sessions, "test.txt", :data => "data") - end - - def test_self_process_should_instantiate_uploader_and_start_process - Capistrano::Upload.expects(:new).with([:s1, :s2], "test.txt", :data => "data").returns(mock(:process! => nil)) - Capistrano::Upload.process([:s1, :s2], "test.txt", :data => "data") - end - - def test_process_when_sftp_open_fails_should_raise_error - sftp = mock_sftp - sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status", :code => "bad status", :message => "bad status"), :file_handle) - session = mock("session", :sftp => sftp, :xserver => server("capistrano")) - upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything) - assert_raises(Capistrano::UploadError) { upload.process! } - assert_equal 1, upload.failed - assert_equal 1, upload.completed - end - - def test_process_when_sftp_write_fails_should_raise_error - sftp = mock_sftp - sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status1", :code => Net::SFTP::Session::FX_OK), :file_handle) - sftp.expects(:write).with(:file_handle, "data").yields(mock("status2", :code => "bad status", :message => "bad status")) - session = mock("session", :sftp => sftp, :xserver => server("capistrano")) - upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything) - assert_raises(Capistrano::UploadError) { upload.process! } - assert_equal 1, upload.failed - assert_equal 1, upload.completed - end - - def test_upload_error_should_include_accessor_with_host_array - sftp = mock_sftp - sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status1", :code => Net::SFTP::Session::FX_OK), :file_handle) - sftp.expects(:write).with(:file_handle, "data").yields(mock("status2", :code => "bad status", :message => "bad status")) - session = mock("session", :sftp => sftp, :xserver => server("capistrano")) - upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything) - - begin - upload.process! - flunk "expected an exception to be raised" - rescue Capistrano::UploadError => e - assert e.respond_to?(:hosts) - assert_equal %w(capistrano), e.hosts.map { |h| h.to_s } - end - end - - def test_process_when_sftp_succeeds_should_raise_nothing - sftp = mock_sftp - sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status1", :code => Net::SFTP::Session::FX_OK), :file_handle) - sftp.expects(:write).with(:file_handle, "data").yields(mock("status2", :code => Net::SFTP::Session::FX_OK)) - sftp.expects(:close_handle).with(:file_handle).yields - session = mock("session", :sftp => sftp, :xserver => server("capistrano")) - upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything) - assert_nothing_raised { upload.process! } - assert_equal 0, upload.failed - assert_equal 1, upload.completed - end - - def test_process_should_loop_while_running - con = mock("connection") - con.expects(:process).with(true).times(10) - channel = {} - channel.expects(:connection).returns(con).times(10) - sftp = mock("sftp", :state => :open, :open => nil) - sftp.stubs(:channel).returns(channel) - session = mock("session", :sftp => sftp, :xserver => server("capistrano")) - upload = Capistrano::Upload.new([session], "test.txt", :data => "data") - upload.expects(:running?).times(11).returns(*([true]*10 + [false])) - upload.process! - end - - def test_process_should_loop_but_not_process_done_channels - new_sftp = Proc.new do |done| - channel = {} - channel[:needs_done] = done - - if !done - con = mock("connection") - con.expects(:process).with(true).times(10) - channel.expects(:connection).returns(con).times(10) - end - - sftp = mock("sftp", :state => :open, :open => nil) - sftp.stubs(:channel).returns(channel) - sftp - end - - sessions = [stub("session", :sftp => new_sftp[true], :xserver => server("capistrano")), - stub("session", :sftp => new_sftp[false], :xserver => server("cap2"))] - upload = Capistrano::Upload.new(sessions, "test.txt", :data => "data") - - # make sure the sftp channels we wanted to be done, start as done - # (Upload.new marks each channel as not-done, so we have to do it here) - sessions.each { |s| s.sftp.channel[:done] = true if s.sftp.channel[:needs_done] } - upload.expects(:running?).times(11).returns(*([true]*10 + [false])) - upload.process! - end - - private - - def mock_sftp - sftp = mock("sftp", :state => :open) - sftp.stubs(:channel).returns(Hash.new) - yield sftp if block_given? - sftp - end -end