require "common"

class SessionTest < Net::SFTP::TestCase
  include Net::SFTP::Constants
  include Net::SFTP::Constants::OpenFlags
  include Net::SFTP::Constants::PacketTypes

  (1..6).each do |version|
    define_method("test_server_reporting_version_#{version}_should_cause_version_#{version}_to_be_used") do
      expect_sftp_session :server_version => version
      assert_scripted { sftp.connect! }
      assert_equal version, sftp.protocol.version
    end
  end

  def test_passing_version_should_cause_same_version_to_be_passed_and_used
    version = 3
    expect_sftp_session :client_version => version, :server_version => version
    assert_scripted { sftp({},version).connect! }
    assert_equal version, sftp.protocol.version
  end


  def test_v1_open_read_only_that_succeeds_should_invoke_callback
    expect_open("/path/to/file", "r", nil, :server_version => 1)
    assert_successful_open("/path/to/file")
  end

  def test_v1_open_read_only_that_fails_should_invoke_callback
    expect_open("/path/to/file", "r", nil, :server_version => 1, :fail => 2)

    assert_command_with_callback(:open, "/path/to/file") do |response|
      assert !response.ok?
      assert_equal 2, response.code
    end
  end

  def test_v1_open_write_only_that_succeeds_should_invoke_callback
    expect_open("/path/to/file", "w", nil, :server_version => 1)
    assert_successful_open("/path/to/file", "w")
  end

  def test_v1_open_read_write_that_succeeds_should_invoke_callback
    expect_open("/path/to/file", "rw", nil, :server_version => 1)
    assert_successful_open("/path/to/file", "r+")
  end

  def test_v1_open_append_that_succeeds_should_invoke_callback
    expect_open("/path/to/file", "a", nil, :server_version => 1)
    assert_successful_open("/path/to/file", "a")
  end

  def test_v1_open_with_permissions_should_specify_permissions
    expect_open("/path/to/file", "r", 0765, :server_version => 1)
    assert_successful_open("/path/to/file", "r", :permissions => 0765)
  end

  def test_v4_open_with_permissions_should_specify_permissions
    expect_open("/path/to/file", "r", 0765, :server_version => 4)
    assert_successful_open("/path/to/file", "r", :permissions => 0765)
  end

  def test_v5_open_read_only_shuld_invoke_callback
    expect_open("/path/to/file", "r", 0765, :server_version => 5)
    assert_successful_open("/path/to/file", "r", :permissions => 0765)
  end

  def test_v6_open_with_permissions_should_specify_permissions
    expect_open("/path/to/file", "r", 0765, :server_version => 6)
    assert_successful_open("/path/to/file", "r", :permissions => 0765)
  end

  def test_open_bang_should_block_and_return_handle
    expect_open("/path/to/file", "r", nil)
    handle = assert_synchronous_command(:open!, "/path/to/file", "r")
    assert_equal "handle", handle
  end

  def test_open_bang_should_block_and_raise_exception_on_error
    expect_open("/path/to/file", "r", nil, :fail => 5)
    assert_raises(Net::SFTP::StatusException) do
      assert_synchronous_command(:open!, "/path/to/file", "r")
    end
  end

  def test_close_should_send_close_request_and_invoke_callback
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_CLOSE, :long, 0, :string, "handle")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:close, "handle") { |r| assert r.ok? }
  end

  def test_close_bang_should_block_and_return_response
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_CLOSE, :long, 0, :string, "handle")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:close!, "handle")
    assert response.ok?
  end

  def test_read_should_send_read_request_and_invoke_callback
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_READ, :long, 0, :string, "handle", :int64, 512123, :long, 1024)
      channel.gets_packet(FXP_DATA, :long, 0, :string, "this is some data!")
    end

    assert_command_with_callback(:read, "handle", 512123, 1024) do |response|
      assert response.ok?
      assert_equal "this is some data!", response[:data]
    end
  end

  def test_read_bang_should_block_and_return_data
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_READ, :long, 0, :string, "handle", :int64, 512123, :long, 1024)
      channel.gets_packet(FXP_DATA, :long, 0, :string, "this is some data!")
    end

    data = assert_synchronous_command(:read!, "handle", 512123, 1024)
    assert_equal "this is some data!", data
  end

  def test_read_bang_should_block_and_return_nil_on_eof
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_READ, :long, 0, :string, "handle", :int64, 512123, :long, 1024)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 1)
    end

    data = assert_synchronous_command(:read!, "handle", 512123, 1024)
    assert_nil data
  end

  def test_write_should_send_write_request_and_invoke_callback
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_WRITE, :long, 0, :string, "handle", :int64, 512123, :string, "this is some data!")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:write, "handle", 512123, "this is some data!") do |response|
      assert response.ok?
    end
  end

  def test_write_bang_should_block_and_return_response
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_WRITE, :long, 0, :string, "handle", :int64, 512123, :string, "this is some data!")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:write!, "handle", 512123, "this is some data!")
    assert response.ok?
  end

  def test_v1_lstat_should_send_lstat_request_and_invoke_callback
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_LSTAT, :long, 0, :string, "/path/to/file")
      channel.gets_packet(FXP_ATTRS, :long, 0, :long, 0xF, :int64, 123456, :long, 1, :long, 2, :long, 0765, :long, 123456789, :long, 234567890)
    end

    assert_command_with_callback(:lstat, "/path/to/file") do |response|
      assert response.ok?
      assert_equal 123456, response[:attrs].size
      assert_equal 1, response[:attrs].uid
      assert_equal 2, response[:attrs].gid
      assert_equal 0765, response[:attrs].permissions
      assert_equal 123456789, response[:attrs].atime
      assert_equal 234567890, response[:attrs].mtime
    end
  end

  def test_v4_lstat_should_send_default_flags_parameter
    expect_sftp_session :server_version => 4 do |channel|
      channel.sends_packet(FXP_LSTAT, :long, 0, :string, "/path/to/file", :long, 0x800001fd)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 2)
    end

    assert_command_with_callback(:lstat, "/path/to/file")
  end

  def test_v4_lstat_should_honor_flags_parameter
    expect_sftp_session :server_version => 4 do |channel|
      channel.sends_packet(FXP_LSTAT, :long, 0, :string, "/path/to/file", :long, 0x1)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 2)
    end

    assert_command_with_callback(:lstat, "/path/to/file", 0x1)
  end

  def test_lstat_bang_should_block_and_return_attrs
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_LSTAT, :long, 0, :string, "/path/to/file")
      channel.gets_packet(FXP_ATTRS, :long, 0, :long, 0xF, :int64, 123456, :long, 1, :long, 2, :long, 0765, :long, 123456789, :long, 234567890)
    end

    attrs = assert_synchronous_command(:lstat!, "/path/to/file")

    assert_equal 123456, attrs.size
    assert_equal 1, attrs.uid
    assert_equal 2, attrs.gid
    assert_equal 0765, attrs.permissions
    assert_equal 123456789, attrs.atime
    assert_equal 234567890, attrs.mtime
  end

  def test_v1_fstat_should_send_fstat_request_and_invoke_callback
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_FSTAT, :long, 0, :string, "handle")
      channel.gets_packet(FXP_ATTRS, :long, 0, :long, 0xF, :int64, 123456, :long, 1, :long, 2, :long, 0765, :long, 123456789, :long, 234567890)
    end

    assert_command_with_callback(:fstat, "handle") do |response|
      assert response.ok?
      assert_equal 123456, response[:attrs].size
      assert_equal 1, response[:attrs].uid
      assert_equal 2, response[:attrs].gid
      assert_equal 0765, response[:attrs].permissions
      assert_equal 123456789, response[:attrs].atime
      assert_equal 234567890, response[:attrs].mtime
    end
  end

  def test_v4_fstat_should_send_default_flags_parameter
    expect_sftp_session :server_version => 4 do |channel|
      channel.sends_packet(FXP_FSTAT, :long, 0, :string, "handle", :long, 0x800001fd)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 2)
    end

    assert_command_with_callback(:fstat, "handle")
  end

  def test_v4_fstat_should_honor_flags_parameter
    expect_sftp_session :server_version => 4 do |channel|
      channel.sends_packet(FXP_FSTAT, :long, 0, :string, "handle", :long, 0x1)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 2)
    end

    assert_command_with_callback(:fstat, "handle", 0x1)
  end

  def test_fstat_bang_should_block_and_return_attrs
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_FSTAT, :long, 0, :string, "handle")
      channel.gets_packet(FXP_ATTRS, :long, 0, :long, 0xF, :int64, 123456, :long, 1, :long, 2, :long, 0765, :long, 123456789, :long, 234567890)
    end

    attrs = assert_synchronous_command(:fstat!, "handle")

    assert_equal 123456, attrs.size
    assert_equal 1, attrs.uid
    assert_equal 2, attrs.gid
    assert_equal 0765, attrs.permissions
    assert_equal 123456789, attrs.atime
    assert_equal 234567890, attrs.mtime
  end

  def test_v1_setstat_should_send_v1_attributes
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_SETSTAT, :long, 0, :string, "/path/to/file", :long, 0xc, :long, 0765, :long, 1234567890, :long, 2345678901)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:setstat, "/path/to/file", :permissions => 0765, :atime => 1234567890, :mtime => 2345678901)
  end

  def test_v4_setstat_should_send_v4_attributes
    expect_sftp_session :server_version => 4 do |channel|
      channel.sends_packet(FXP_SETSTAT, :long, 0, :string, "/path/to/file", :long, 0x2c, :byte, 1, :long, 0765, :int64, 1234567890, :int64, 2345678901)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:setstat, "/path/to/file", :permissions => 0765, :atime => 1234567890, :mtime => 2345678901)
  end

  def test_v6_setstat_should_send_v6_attributes
    expect_sftp_session :server_version => 6 do |channel|
      channel.sends_packet(FXP_SETSTAT, :long, 0, :string, "/path/to/file", :long, 0x102c, :byte, 1, :long, 0765, :int64, 1234567890, :int64, 2345678901, :string, "text/plain")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:setstat, "/path/to/file", :permissions => 0765, :atime => 1234567890, :mtime => 2345678901, :mime_type => "text/plain")
  end

  def test_setstat_bang_should_block_and_return_response
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_SETSTAT, :long, 0, :string, "/path/to/file", :long, 0xc, :long, 0765, :long, 1234567890, :long, 2345678901)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:setstat!, "/path/to/file", :permissions => 0765, :atime => 1234567890, :mtime => 2345678901)
    assert response.ok?
  end

  def test_v1_fsetstat_should_send_v1_attributes
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_FSETSTAT, :long, 0, :string, "handle", :long, 0xc, :long, 0765, :long, 1234567890, :long, 2345678901)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:fsetstat, "handle", :permissions => 0765, :atime => 1234567890, :mtime => 2345678901)
  end

  def test_v4_fsetstat_should_send_v4_attributes
    expect_sftp_session :server_version => 4 do |channel|
      channel.sends_packet(FXP_FSETSTAT, :long, 0, :string, "handle", :long, 0x2c, :byte, 1, :long, 0765, :int64, 1234567890, :int64, 2345678901)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:fsetstat, "handle", :permissions => 0765, :atime => 1234567890, :mtime => 2345678901)
  end

  def test_v6_fsetstat_should_send_v6_attributes
    expect_sftp_session :server_version => 6 do |channel|
      channel.sends_packet(FXP_FSETSTAT, :long, 0, :string, "handle", :long, 0x102c, :byte, 1, :long, 0765, :int64, 1234567890, :int64, 2345678901, :string, "text/plain")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:fsetstat, "handle", :permissions => 0765, :atime => 1234567890, :mtime => 2345678901, :mime_type => "text/plain")
  end

  def test_fsetstat_bang_should_block_and_return_response
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_FSETSTAT, :long, 0, :string, "handle", :long, 0xc, :long, 0765, :long, 1234567890, :long, 2345678901)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:fsetstat!, "handle", :permissions => 0765, :atime => 1234567890, :mtime => 2345678901)
    assert response.ok?
  end

  def test_opendir_should_send_opendir_request_and_invoke_callback
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_OPENDIR, :long, 0, :string, "/path/to/dir")
      channel.gets_packet(FXP_HANDLE, :long, 0, :string, "handle")
    end

    assert_command_with_callback(:opendir, "/path/to/dir")
  end

  def test_opendir_bang_should_block_and_return_handle
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_OPENDIR, :long, 0, :string, "/path/to/dir")
      channel.gets_packet(FXP_HANDLE, :long, 0, :string, "handle")
    end

    handle = assert_synchronous_command(:opendir!, "/path/to/dir")
    assert_equal "handle", handle
  end

  def test_readdir_should_send_readdir_request_and_invoke_callback
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_READDIR, :long, 0, :string, "handle")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 1)
    end

    assert_command_with_callback(:readdir, "handle") { |r| assert r.eof? }
  end

  def test_readdir_bang_should_block_and_return_names_array
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_READDIR, :long, 0, :string, "handle")
      channel.gets_packet(FXP_NAME, :long, 0, :long, 2,
        :string, "first", :string, "longfirst", :long, 0x0,
        :string, "next", :string, "longnext", :long, 0x0)
    end

    names = assert_synchronous_command(:readdir!, "handle")
    assert_equal 2, names.length
    assert_equal %w(first next), names.map { |n| n.name }
  end

  def test_remove_should_send_remove_packet
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_REMOVE, :long, 0, :string, "/path/to/file")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:remove, "/path/to/file")
  end

  def test_remove_bang_should_block_and_return_response
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_REMOVE, :long, 0, :string, "/path/to/file")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:remove!, "/path/to/file")
    assert response.ok?
  end

  def test_mkdir_should_send_mkdir_packet
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_MKDIR, :long, 0, :string, "/path/to/dir", :long, 0x4, :byte, 1, :long, 0765)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:mkdir, "/path/to/dir", :permissions => 0765)
  end

  def test_mkdir_bang_should_block_and_return_response
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_MKDIR, :long, 0, :string, "/path/to/dir", :long, 0x4, :byte, 1, :long, 0765)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:mkdir!, "/path/to/dir", :permissions => 0765)
    assert response.ok?
  end

  def test_rmdir_should_send_rmdir_packet
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_RMDIR, :long, 0, :string, "/path/to/dir")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:rmdir, "/path/to/dir")
  end

  def test_rmdir_bang_should_block_and_return_response
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_RMDIR, :long, 0, :string, "/path/to/dir")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:rmdir!, "/path/to/dir")
    assert response.ok?
  end

  def test_realpath_should_send_realpath_packet
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_REALPATH, :long, 0, :string, "/path/to/dir")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 2)
    end

    assert_command_with_callback(:realpath, "/path/to/dir")
  end

  def test_realpath_bang_should_block_and_return_names_item
    expect_sftp_session do |channel|
      channel.sends_packet(FXP_REALPATH, :long, 0, :string, "/path/to/dir")
      channel.gets_packet(FXP_NAME, :long, 0, :long, 1, :string, "dir", :long, 0x0, :long, 2)
    end

    name = assert_synchronous_command(:realpath!, "/path/to/dir")
    assert_equal "dir", name.name
  end

  def test_v1_stat_should_send_stat_request_and_invoke_callback
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_STAT, :long, 0, :string, "/path/to/file")
      channel.gets_packet(FXP_ATTRS, :long, 0, :long, 0xF, :int64, 123456, :long, 1, :long, 2, :long, 0765, :long, 123456789, :long, 234567890)
    end

    assert_command_with_callback(:stat, "/path/to/file") do |response|
      assert response.ok?
      assert_equal 123456, response[:attrs].size
      assert_equal 1, response[:attrs].uid
      assert_equal 2, response[:attrs].gid
      assert_equal 0765, response[:attrs].permissions
      assert_equal 123456789, response[:attrs].atime
      assert_equal 234567890, response[:attrs].mtime
    end
  end

  def test_v4_stat_should_send_default_flags_parameter
    expect_sftp_session :server_version => 4 do |channel|
      channel.sends_packet(FXP_STAT, :long, 0, :string, "/path/to/file", :long, 0x800001fd)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 2)
    end

    assert_command_with_callback(:stat, "/path/to/file")
  end

  def test_v4_stat_should_honor_flags_parameter
    expect_sftp_session :server_version => 4 do |channel|
      channel.sends_packet(FXP_STAT, :long, 0, :string, "/path/to/file", :long, 0x1)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 2)
    end

    assert_command_with_callback(:stat, "/path/to/file", 0x1)
  end

  def test_stat_bang_should_block_and_return_attrs
    expect_sftp_session :server_version => 1 do |channel|
      channel.sends_packet(FXP_STAT, :long, 0, :string, "/path/to/file")
      channel.gets_packet(FXP_ATTRS, :long, 0, :long, 0xF, :int64, 123456, :long, 1, :long, 2, :long, 0765, :long, 123456789, :long, 234567890)
    end

    attrs = assert_synchronous_command(:stat!, "/path/to/file")

    assert_equal 123456, attrs.size
    assert_equal 1, attrs.uid
    assert_equal 2, attrs.gid
    assert_equal 0765, attrs.permissions
    assert_equal 123456789, attrs.atime
    assert_equal 234567890, attrs.mtime
  end

  def test_v1_rename_should_be_unimplemented
    assert_not_implemented 1, :rename, "from", "to"
  end

  def test_v2_rename_should_send_rename_packet
    expect_sftp_session :server_version => 2 do |channel|
      channel.sends_packet(FXP_RENAME, :long, 0, :string, "from", :string, "to")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:rename, "from", "to")
  end

  def test_v5_rename_should_send_rename_packet_and_default_flags
    expect_sftp_session :server_version => 5 do |channel|
      channel.sends_packet(FXP_RENAME, :long, 0, :string, "from", :string, "to", :long, 0)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:rename, "from", "to")
  end

  def test_v5_rename_should_send_rename_packet_and_honor_flags
    expect_sftp_session :server_version => 5 do |channel|
      channel.sends_packet(FXP_RENAME, :long, 0, :string, "from", :string, "to", :long, 1)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:rename, "from", "to", 1)
  end

  def test_rename_bang_should_block_and_return_response
    expect_sftp_session :server_version => 2 do |channel|
      channel.sends_packet(FXP_RENAME, :long, 0, :string, "from", :string, "to")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:rename!, "from", "to")
    assert response.ok?
  end

  def test_v2_readlink_should_be_unimplemented
    assert_not_implemented 2, :readlink, "/path/to/link"
  end

  def test_v3_readlink_should_send_readlink_packet
    expect_sftp_session :server_version => 3 do |channel|
      channel.sends_packet(FXP_READLINK, :long, 0, :string, "/path/to/link")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 2)
    end

    assert_command_with_callback(:readlink, "/path/to/link")
  end

  def test_readlink_bang_should_block_and_return_name
    expect_sftp_session :server_version => 3 do |channel|
      channel.sends_packet(FXP_READLINK, :long, 0, :string, "/path/to/link")
      channel.gets_packet(FXP_NAME, :long, 0, :long, 1, :string, "target", :string, "longtarget", :long, 0x0)
    end

    name = assert_synchronous_command(:readlink!, "/path/to/link")
    assert_equal "target", name.name
  end

  def test_v2_symlink_should_be_unimplemented
    assert_not_implemented 2, :symlink, "/path/to/source", "/path/to/link"
  end

  def test_v3_symlink_should_send_symlink_packet
    expect_sftp_session :server_version => 3 do |channel|
      channel.sends_packet(FXP_SYMLINK, :long, 0, :string, "/path/to/source", :string, "/path/to/link")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:symlink, "/path/to/source", "/path/to/link")
  end

  def test_v6_symlink_should_send_link_packet
    expect_sftp_session :server_version => 6 do |channel|
      channel.sends_packet(FXP_LINK, :long, 0, :string, "/path/to/link", :string, "/path/to/source", :bool, true)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:symlink, "/path/to/link", "/path/to/source")
  end

  def test_symlink_bang_should_block_and_return_response
    expect_sftp_session :server_version => 3 do |channel|
      channel.sends_packet(FXP_SYMLINK, :long, 0, :string, "/path/to/source", :string, "/path/to/link")
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:symlink!, "/path/to/source", "/path/to/link")
    assert response.ok?
  end

  def test_v5_link_should_be_unimplemented
    assert_not_implemented 5, :link, "/path/to/source", "/path/to/link", true
  end

  def test_v6_link_should_send_link_packet
    expect_sftp_session :server_version => 6 do |channel|
      channel.sends_packet(FXP_LINK, :long, 0, :string, "/path/to/link", :string, "/path/to/source", :bool, true)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:link, "/path/to/link", "/path/to/source", true)
  end

  def test_link_bang_should_block_and_return_response
    expect_sftp_session :server_version => 6 do |channel|
      channel.sends_packet(FXP_LINK, :long, 0, :string, "/path/to/link", :string, "/path/to/source", :bool, true)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:link!, "/path/to/link", "/path/to/source", true)
    assert response.ok?
  end

  def test_v5_block_should_be_unimplemented
    assert_not_implemented 5, :block, "handle", 12345, 67890, 0xabcd
  end

  def test_v6_block_should_send_block_packet
    expect_sftp_session :server_version => 6 do |channel|
      channel.sends_packet(FXP_BLOCK, :long, 0, :string, "handle", :int64, 12345, :int64, 67890, :long, 0xabcd)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:block, "handle", 12345, 67890, 0xabcd)
  end

  def test_block_bang_should_block_and_return_response
    expect_sftp_session :server_version => 6 do |channel|
      channel.sends_packet(FXP_BLOCK, :long, 0, :string, "handle", :int64, 12345, :int64, 67890, :long, 0xabcd)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:block!, "handle", 12345, 67890, 0xabcd)
    assert response.ok?
  end

  def test_v5_unblock_should_be_unimplemented
    assert_not_implemented 5, :unblock, "handle", 12345, 67890
  end

  def test_v6_unblock_should_send_block_packet
    expect_sftp_session :server_version => 6 do |channel|
      channel.sends_packet(FXP_UNBLOCK, :long, 0, :string, "handle", :int64, 12345, :int64, 67890)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    assert_command_with_callback(:unblock, "handle", 12345, 67890)
  end

  def test_unblock_bang_should_block_and_return_response
    expect_sftp_session :server_version => 6 do |channel|
      channel.sends_packet(FXP_UNBLOCK, :long, 0, :string, "handle", :int64, 12345, :int64, 67890)
      channel.gets_packet(FXP_STATUS, :long, 0, :long, 0)
    end

    response = assert_synchronous_command(:unblock!, "handle", 12345, 67890)
    assert response.ok?
  end

  private

    def assert_not_implemented(server_version, command, *args)
      expect_sftp_session :server_version => 1
      Net::SSH::Test::Extensions::IO.with_test_extension do
        sftp.connect!
        assert_raises(NotImplementedError) { sftp.send(command, *args) }
      end
    end

    def assert_command_with_callback(command, *args)
      called = false
      assert_scripted_command do
        sftp.send(command, *args) do |response|
          called = true
          yield response if block_given?
        end
      end
      assert called, "expected callback to be invoked, but it wasn't"
    end

    def assert_synchronous_command(command, *args)
      assert_scripted_command do
        sequence = [:start]
        result = sftp.send(command, *args) do |response|
          sequence << :done
          yield response if block_given?
        end
        sequence << :after
        assert_equal [:start, :done, :after], sequence, "expected #{command} to be synchronous"
        return result
      end
    end

    def assert_successful_open(*args)
      assert_command_with_callback(:open, *args) do |response|
        assert response.ok?
        assert_equal "handle", response[:handle]
      end
    end

    def expect_open(path, mode, perms, options={})
      version = options[:server_version] || 6

      fail = options.delete(:fail)

      attrs = [:long, perms ? 0x4 : 0]
      attrs += [:byte, 1] if version >= 4
      attrs += [:long, perms] if perms

      expect_sftp_session(options) do |channel|
        if version >= 5
          flags, access = case mode
            when "r" then 
              [FV5::OPEN_EXISTING, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES]
            when "w" then
              [FV5::CREATE_TRUNCATE, ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES]
            when "rw" then
              [FV5::OPEN_OR_CREATE, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES | ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES]
            when "a" then
              [FV5::OPEN_OR_CREATE | FV5::APPEND_DATA, ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES | ACE::Mask::APPEND_DATA]
            else raise ArgumentError, "unsupported mode #{mode.inspect}"
          end

          channel.sends_packet(FXP_OPEN, :long, 0, :string, path, :long, access, :long, flags, *attrs)
        else
          flags = case mode
            when "r"  then FV1::READ
            when "w"  then FV1::WRITE | FV1::TRUNC | FV1::CREAT
            when "rw" then FV1::WRITE | FV1::READ
            when "a"  then FV1::APPEND | FV1::WRITE | FV1::CREAT
            else raise ArgumentError, "unsupported mode #{mode.inspect}"
          end

          channel.sends_packet(FXP_OPEN, :long, 0, :string, path, :long, flags, *attrs)
        end

        if fail
          channel.gets_packet(FXP_STATUS, :long, 0, :long, fail)
        else
          channel.gets_packet(FXP_HANDLE, :long, 0, :string, "handle")
        end
      end
    end
end
