require 'spec_helper'

RSpec.describe RubySMB::Client do
  let(:sock) { double('Socket', peeraddr: '192.168.1.5') }
  let(:dispatcher) { RubySMB::Dispatcher::Socket.new(sock) }
  let(:username) { 'msfadmin' }
  let(:password) { 'msfpasswd' }
  subject(:client) { described_class.new(dispatcher, username: username, password: password) }
  let(:smb1_client) { described_class.new(dispatcher, smb2: false, smb3: false, username: username, password: password) }
  let(:smb2_client) { described_class.new(dispatcher, smb1: false, smb3: false, username: username, password: password) }
  let(:smb3_client) { described_class.new(dispatcher, smb1: false, smb2: false, username: username, password: password) }
  let(:empty_packet) { RubySMB::SMB1::Packet::EmptyPacket.new }
  let(:error_packet) { RubySMB::SMB2::Packet::ErrorPacket.new }

  it { is_expected.to respond_to :dispatcher }
  it { is_expected.to respond_to :domain }
  it { is_expected.to respond_to :local_workstation }
  it { is_expected.to respond_to :ntlm_client }
  it { is_expected.to respond_to :password }
  it { is_expected.to respond_to :peer_native_os }
  it { is_expected.to respond_to :peer_native_lm }
  it { is_expected.to respond_to :primary_domain }
  it { is_expected.to respond_to :default_name }
  it { is_expected.to respond_to :default_domain }
  it { is_expected.to respond_to :dns_host_name }
  it { is_expected.to respond_to :dns_domain_name }
  it { is_expected.to respond_to :dns_tree_name }
  it { is_expected.to respond_to :os_version }
  it { is_expected.to respond_to :dialect }
  it { is_expected.to respond_to :sequence_counter }
  it { is_expected.to respond_to :session_id }
  it { is_expected.to respond_to :signing_required }
  it { is_expected.to respond_to :smb1 }
  it { is_expected.to respond_to :smb2 }
  it { is_expected.to respond_to :smb3 }
  it { is_expected.to respond_to :smb2_message_id }
  it { is_expected.to respond_to :username }
  it { is_expected.to respond_to :user_id }
  it { is_expected.to respond_to :max_buffer_size }
  it { is_expected.to respond_to :server_max_buffer_size }
  it { is_expected.to respond_to :server_max_write_size }
  it { is_expected.to respond_to :server_max_read_size }
  it { is_expected.to respond_to :server_max_transact_size }
  it { is_expected.to respond_to :preauth_integrity_hash_algorithm }
  it { is_expected.to respond_to :preauth_integrity_hash_value }
  it { is_expected.to respond_to :encryption_algorithm }
  it { is_expected.to respond_to :client_encryption_key }
  it { is_expected.to respond_to :server_encryption_key }
  it { is_expected.to respond_to :session_encrypt_data }
  it { is_expected.to respond_to :server_encryption_algorithms }
  it { is_expected.to respond_to :server_compression_algorithms }
  it { is_expected.to respond_to :negotiated_smb_version }
  it { is_expected.to respond_to :session_key }
  it { is_expected.to respond_to :tree_connects }
  it { is_expected.to respond_to :open_files }
  it { is_expected.to respond_to :use_ntlmv2 }
  it { is_expected.to respond_to :usentlm2_session }
  it { is_expected.to respond_to :send_lm }
  it { is_expected.to respond_to :use_lanman_key }
  it { is_expected.to respond_to :send_ntlm }
  it { is_expected.to respond_to :spnopt }
  it { is_expected.to respond_to :evasion_opts }
  it { is_expected.to respond_to :native_os }
  it { is_expected.to respond_to :native_lm }
  it { is_expected.to respond_to :verify_signature }
  it { is_expected.to respond_to :auth_user }
  it { is_expected.to respond_to :last_file_id }
  it { is_expected.to respond_to :pid }
  it { is_expected.to respond_to :server_supports_multi_credit }

  describe '#initialize' do
    it 'should raise an ArgumentError without a valid dispatcher' do
      expect { described_class.new(nil) }.to raise_error(ArgumentError)
    end

    it 'defaults to true for SMB1 support' do
      expect(client.smb1).to be true
    end

    it 'defaults to true for SMB2 support' do
      expect(client.smb1).to be true
    end

    it 'accepts an argument to disable smb1 support' do
      expect(smb2_client.smb1).to be false
      expect(smb3_client.smb1).to be false
    end

    it 'accepts an argument to disable smb2 support' do
      expect(smb1_client.smb2).to be false
      expect(smb3_client.smb2).to be false
    end

    it 'accepts an argument to disable smb3 support' do
      expect(smb1_client.smb3).to be false
      expect(smb2_client.smb3).to be false
    end

    it 'raises an exception if SMB1, SMB2 and SMB3 are disabled' do
      expect { described_class.new(dispatcher, smb1: false, smb2: false, smb3: false, username: username, password: password) }.to raise_error(ArgumentError, 'You must enable at least one Protocol')
    end

    it 'sets the username attribute' do
      expect(client.username).to eq username
    end

    it 'sets the password attribute' do
      expect(client.password).to eq password
    end

    it 'sets the session_encrypt_data attribute' do
      client =  described_class.new(dispatcher, username: username, password: password, always_encrypt: true)
      expect(client.session_encrypt_data).to eq true
    end

    it 'creates an NTLM client' do
      expect(client.ntlm_client).to be_a Net::NTLM::Client
    end

    it 'passes the expected arguments when creating the NTLM client' do
      domain = 'SPEC_DOMAIN'
      local_workstation = 'SPEC_WORKSTATION'

      allow(Net::NTLM::Client).to receive(:new) do |username, passwd, opt|
        expect(username).to eq(username)
        expect(password).to eq(password)
        expect(opt[:workstation]).to eq(local_workstation)
        expect(opt[:domain]).to eq(domain)
        flags = RubySMB::NTLM::DEFAULT_CLIENT_FLAGS
        expect(opt[:flags]).to eq(flags)
      end

      described_class.new(
        dispatcher,
        username: username,
        password: password,
        domain: domain,
        local_workstation: local_workstation
      )
    end

    it 'sets the max_buffer_size to MAX_BUFFER_SIZE' do
      expect(client.max_buffer_size).to eq RubySMB::Client::MAX_BUFFER_SIZE
    end

    it 'sets the server_supports_multi_credit to false' do
      expect(client.server_supports_multi_credit).to be false
    end

    it 'sets the pid to a random value' do
      100.times do
        previous_pid = client.pid
        client = described_class.new(dispatcher, username: username, password: password)
        expect(client.pid).to_not eq(previous_pid)
      end
    end
  end

  describe '#echo' do
    let(:response) { double('Echo Response') }
    before :example do
      allow(response).to receive(:status_code).and_return(WindowsError::NTStatus::STATUS_SUCCESS)
    end

    context 'with SMB1' do
      it 'calls #smb1_echo with the expected arguments' do
        allow(smb1_client).to receive(:smb1_echo).and_return(response)
        count = 3
        data  = 'testing...'
        smb1_client.echo(count: count, data: data)
        expect(smb1_client).to have_received(:smb1_echo).with(count: count, data: data)
      end
    end

    context 'with SMB2' do
      it 'calls #smb2_echo without arguments' do
        allow(smb2_client).to receive(:smb2_echo).and_return(response)
        smb2_client.echo
        expect(smb2_client).to have_received(:smb2_echo)
      end
    end

    context 'with SMB3' do
      it 'calls #smb2_echo without arguments' do
        allow(smb3_client).to receive(:smb2_echo).and_return(response)
        smb3_client.echo
        expect(smb3_client).to have_received(:smb2_echo)
      end
    end

    it 'returns the expected status code' do
      allow(smb2_client).to receive(:smb2_echo).and_return(response)
      expect(smb2_client.echo).to eq(WindowsError::NTStatus::STATUS_SUCCESS)
    end
  end

  describe '#send_recv' do
    let(:smb1_request) { RubySMB::SMB1::Packet::TreeConnectRequest.new }
    let(:smb2_request) { RubySMB::SMB2::Packet::TreeConnectRequest.new }
    let(:smb2_header)  { RubySMB::SMB2::SMB2Header.new }

    before(:each) do
      allow(client).to receive(:is_status_pending?).and_return(false)
      allow(dispatcher).to receive(:send_packet).and_return(nil)
      allow(dispatcher).to receive(:recv_packet).and_return('A')
      allow(RubySMB::SMB2::SMB2Header).to receive(:read).and_return(smb2_header)
    end

    context 'when signing' do
      it 'calls #smb1_sign if it is an SMB1 packet' do
        allow(client).to receive(:signing_required).and_return(true)
        allow(client).to receive(:session_key).and_return(Random.new.bytes(16))
        expect(client).to receive(:smb1_sign).with(smb1_request).and_call_original
        client.send_recv(smb1_request)
      end

      context 'with an SMB2 packet' do
        it 'does not sign a SessionSetupRequest packet' do
          allow(smb2_client).to receive(:is_status_pending?).and_return(false)
          expect(smb2_client).to_not receive(:smb2_sign)
          expect(smb2_client).to_not receive(:smb3_sign)
          smb2_client.send_recv(RubySMB::SMB2::Packet::SessionSetupRequest.new)
        end

        it 'calls #smb2_sign if it is an SMB2 client' do
          allow(smb2_client).to receive(:is_status_pending?).and_return(false)
          allow(smb2_client).to receive(:signing_required).and_return(true)
          allow(smb2_client).to receive(:session_key).and_return(Random.new.bytes(16))
          expect(smb2_client).to receive(:smb2_sign).with(smb2_request).and_call_original
          smb2_client.send_recv(smb2_request)
        end
      end
    end

    it 'sends the expected packet and gets the response' do
      expect(dispatcher).to receive(:send_packet).with(smb1_request)
      expect(dispatcher).to receive(:recv_packet)
      client.send_recv(smb1_request)
    end

    context 'with SMB1' do
      it 'does not check if it is a STATUS_PENDING response' do
        expect(smb1_client).to_not receive(:is_status_pending?)
        smb1_client.send_recv(smb1_request)
      end

      it 'set the #uid SMB header when #user_id is defined' do
        smb1_client.user_id = 333
        smb1_client.send_recv(smb1_request)
        expect(smb1_request.smb_header.uid).to eq(333)
      end

      it 'does not set the #uid SMB header when #user_id is not defined' do
        smb1_client.send_recv(smb1_request)
        expect(smb1_request.smb_header.uid).to eq(0)
      end

      it 'set the #pid SMB header when #pid is defined' do
        smb1_client.pid = 333
        smb1_client.send_recv(smb1_request)
        expect(smb1_request.smb_header.pid_low).to eq(333)
      end

      it 'does not set the #pid SMB header when #pid is not defined' do
        smb1_client.pid = nil
        smb1_client.send_recv(smb1_request)
        expect(smb1_request.smb_header.pid_low).to eq(0)
      end
    end

    context 'with SMB2' do
      context 'when receiving a STATUS_PENDING response' do
        it 'waits 1 second and reads/decrypts again' do
          allow(smb2_client).to receive(:is_status_pending?).and_return(true, false)
          expect(smb2_client).to receive(:sleep).with(1)
          expect(dispatcher).to receive(:recv_packet).twice
          smb2_client.send_recv(smb2_request)
        end
      end
    end

    context 'with SMB3 and encryption' do
      before :example do
        smb3_client.dialect = '0x0300'
        allow(smb3_client).to receive(:is_status_pending?).and_return(false)
      end

      context 'with a SessionSetupRequest' do
        it 'does not encrypt/decrypt' do
          request = RubySMB::SMB2::Packet::SessionSetupRequest.new
          expect(smb3_client).to receive(:send_packet).with(request, encrypt: false)
          expect(smb3_client).to receive(:recv_packet).with(encrypt: false)
          smb3_client.send_recv(request)
        end
      end

      context 'with a NegotiateRequest' do
        it 'does not encrypt/decrypt' do
          request = RubySMB::SMB2::Packet::NegotiateRequest.new
          expect(smb3_client).to receive(:send_packet).with(request, encrypt: false)
          expect(smb3_client).to receive(:recv_packet).with(encrypt: false)
          smb3_client.send_recv(request)
        end
      end

      it 'encrypts and decrypts' do
        expect(smb3_client).to receive(:send_packet).with(smb2_request, encrypt: true)
        expect(smb3_client).to receive(:recv_packet).with(encrypt: true)
        smb3_client.send_recv(smb2_request)
      end

      context 'when receiving a STATUS_PENDING response' do
        it 'waits 1 second and reads/decrypts again' do
          allow(smb3_client).to receive(:is_status_pending?).and_return(true, false)
          expect(smb3_client).to receive(:sleep).with(1)
          expect(smb3_client).to receive(:send_packet).with(smb2_request, encrypt: true)
          expect(smb3_client).to receive(:recv_packet).with(encrypt: true).twice
          smb3_client.send_recv(smb2_request)
        end
      end
    end

    it 'increments the sequence counter if signing is required and the session key exist' do
      allow(smb2_client).to receive(:is_status_pending?).and_return(false)
      smb2_client.signing_required = true
      smb2_client.session_key = 'key'
      smb2_client.sequence_counter = 0
      smb2_client.send_recv(smb2_request)
      expect(smb2_client.sequence_counter).to eq(1)
    end

    it 'updates #smb2_message_id with SMB2 header #credit_charge if the server supports multi credits' do
      allow(smb2_client).to receive(:is_status_pending?).and_return(false)
      smb2_client.smb2_message_id = 0
      smb2_client.server_supports_multi_credit = true
      smb2_header.credit_charge = 5
      smb2_client.send_recv(smb2_request)
      expect(smb2_client.smb2_message_id).to eq(5)
    end

    it 'does not update #msb2_message_id with SMB2 header #credit_charge if the server does not support multi credits' do
      allow(smb2_client).to receive(:is_status_pending?).and_return(false)
      smb2_client.smb2_message_id = 0
      smb2_client.server_supports_multi_credit = false
      smb2_header.credit_charge = 5
      smb2_client.send_recv(smb2_request)
      expect(smb2_client.smb2_message_id).to eq(1)
    end

    it 'ignores errors thrown when parsing the SMB2 header' do
      allow(RubySMB::SMB2::SMB2Header).to receive(:read).and_raise(IOError)
      expect { smb2_client.send_recv(smb2_request) }.to_not raise_error
    end
  end

  describe '#is_status_pending?' do
    let(:response) {
      res = RubySMB::SMB2::Packet::SessionSetupRequest.new
      res.smb2_header.nt_status= WindowsError::NTStatus::STATUS_PENDING.value
      res.smb2_header.flags.async_command = 1
      res
    }

    it 'returns true when the response has a STATUS_PENDING status code and the async_command flag set' do
      expect(client.is_status_pending?(response.smb2_header)).to be true
    end

    it 'returns false when the response has a STATUS_PENDING status code and the async_command flag not set' do
      response.smb2_header.flags.async_command = 0
      expect(client.is_status_pending?(response.smb2_header)).to be false
    end

    it 'returns false when the response has no STATUS_PENDING status code but the async_command flag set' do
      response.smb2_header.nt_status= WindowsError::NTStatus::STATUS_SUCCESS.value
      expect(client.is_status_pending?(response.smb2_header)).to be false
    end
  end

  describe '#can_be_encrypted?' do
    it 'returns true if the packet can be encrypted' do
      packet = RubySMB::SMB2::Packet::TreeConnectRequest.new
      expect(client.can_be_encrypted?(packet)).to be true
    end

    it 'returns false if it is an SMB1 packet' do
      packet = RubySMB::SMB1::Packet::LogoffRequest.new
      expect(client.can_be_encrypted?(packet)).to be false
    end

    [RubySMB::SMB2::Packet::SessionSetupRequest, RubySMB::SMB2::Packet::NegotiateRequest].each do |klass|
      it "returns false if the packet is a #{klass}" do
        packet = klass.new
        expect(client.can_be_encrypted?(packet)).to be false
      end
    end
  end

  describe '#encryption_supported?' do
    ['0x0300', '0x0302', '0x0311'].each do |dialect|
      it "returns true if the dialect is #{dialect}" do
        client.dialect = dialect
        expect(client.encryption_supported?).to be true
      end
    end

    it "returns false if the dialect does not support encryption" do
      client.dialect = '0x0202'
      expect(client.encryption_supported?).to be false
    end
  end

  describe '#send_packet' do
    let(:packet) { RubySMB::SMB2::Packet::SessionSetupRequest.new }
    before :example do
      allow(dispatcher).to receive(:send_packet)
      client.dialect = '0x0300'
    end

    it 'does not encrypt the packet' do
      expect(client).to_not receive(:smb3_encrypt)
      client.send_packet(packet)
    end

    it 'sends the packet through the dispatcher' do
      client.send_packet(packet)
      expect(dispatcher).to have_received(:send_packet).with(packet)
    end

    context 'with encryption' do
      it 'creates a Transform request' do
        expect(client).to receive(:smb3_encrypt).with(packet.to_binary_s)
        client.send_packet(packet, encrypt: true)
      end

      it 'raises an EncryptionError exception if an error occurs while encrypting' do
        allow(client).to receive(:smb3_encrypt).and_raise(RubySMB::Error::RubySMBError.new('Error'))
        expect { client.send_packet(packet, encrypt: true) }.to raise_error(
          RubySMB::Error::EncryptionError,
          "Error while encrypting #{packet.class.name} packet (SMB 0x0300): Error"
        )
      end

      it 'sends the encrypted packet' do
        encrypted_packet = double('Encrypted packet')
        allow(client).to receive(:smb3_encrypt).and_return(encrypted_packet)
        client.send_packet(packet, encrypt: true)
        expect(dispatcher).to have_received(:send_packet).with(encrypted_packet)
      end
    end
  end

  describe '#recv_packet' do
    let(:packet) { RubySMB::SMB2::Packet::SessionSetupRequest.new }
    before :example do
      allow(dispatcher).to receive(:recv_packet).and_return(packet.to_binary_s)
      client.dialect = '0x0300'
      allow(client).to receive(:smb3_decrypt)
    end

    it 'reads the response packet' do
      client.recv_packet
      expect(dispatcher).to have_received(:recv_packet)
    end

    it 'raises an CommunicationError exception if an error occurs while receiving the response' do
      allow(dispatcher).to receive(:recv_packet).and_raise(RubySMB::Error::CommunicationError)
      expect { client.recv_packet }.to raise_error(RubySMB::Error::CommunicationError)
    end

    context 'with encryption' do
      it 'raises an EncryptionError exception if an error occurs while receiving the response' do
        allow(dispatcher).to receive(:recv_packet).and_raise(RubySMB::Error::CommunicationError)
        expect { client.recv_packet(encrypt: true) }.to raise_error(
          RubySMB::Error::CommunicationError,
          'Communication error with the remote host: RubySMB::Error::CommunicationError. '\
          'The server supports encryption and this error may have been caused by encryption issues, but not always.'
        )
      end

      it 'parses the response as a Transform response packet' do
        expect(RubySMB::SMB2::Packet::TransformHeader).to receive(:read).with(packet.to_binary_s)
        client.recv_packet(encrypt: true)
      end

      it 'raises an InvalidPacket exception if an error occurs while parsing the response' do
        allow(RubySMB::SMB2::Packet::TransformHeader).to receive(:read).and_raise(IOError)
        expect { client.recv_packet(encrypt: true) }.to raise_error(RubySMB::Error::InvalidPacket, 'Not a SMB2 TransformHeader packet')
      end

      it 'decrypts the Transform response packet' do
        transform = double('Transform header packet')
        allow(RubySMB::SMB2::Packet::TransformHeader).to receive(:read).and_return(transform)
        client.recv_packet(encrypt: true)
        expect(client).to have_received(:smb3_decrypt).with(transform)
      end

      it 'raises an EncryptionError exception if an error occurs while decrypting' do
        allow(client).to receive(:smb3_decrypt).and_raise(RubySMB::Error::RubySMBError)
        expect { client.recv_packet(encrypt: true) }.to raise_error(
          RubySMB::Error::EncryptionError,
          'Error while decrypting RubySMB::SMB2::Packet::TransformHeader packet (SMB 0x0300}): RubySMB::Error::RubySMBError'
        )
      end
    end
  end

  describe '#login' do
    before(:each) do
      allow(client).to receive(:negotiate)
      allow(client).to receive(:authenticate)
    end

    it 'defaults username to what was in the initializer' do
      expect { client.login }.to_not change(client, :username)
    end

    it 'overrides username if it is passed as a parameter' do
      expect { client.login(username: 'test') }.to change(client, :username).to('test')
    end

    it 'defaults password to what was in the initializer' do
      expect { client.login }.to_not change(client, :password)
    end

    it 'overrides password if it is passed as a parameter' do
      expect { client.login(password: 'test') }.to change(client, :password).to('test')
    end

    it 'defaults domain to what was in the initializer' do
      expect { client.login }.to_not change(client, :domain)
    end

    it 'overrides domain if it is passed as a parameter' do
      expect { client.login(domain: 'test') }.to change(client, :domain).to('test')
    end

    it 'defaults local_workstation to what was in the initializer' do
      expect { client.login }.to_not change(client, :local_workstation)
    end

    it 'overrides local_workstation if it is passed as a parameter' do
      expect { client.login(local_workstation: 'test') }.to change(client, :local_workstation).to('test')
    end

    it 'initialises a new NTLM Client' do
      expect { client.login }.to change(client, :ntlm_client)
    end

    it 'calls negotiate after the setup' do
      expect(client).to receive(:negotiate)
      client.login
    end

    it 'calls authenticate after negotiate' do
      expect(client).to receive(:authenticate)
      client.login
    end
  end

  describe '#wipe_state!' do
    context 'with SMB1' do
      it { expect { smb1_client.wipe_state! }.to_not raise_error }
    end

    context 'with SMB2' do
      it { expect { smb2_client.wipe_state! }.to_not raise_error }
    end
  end

  describe '#logoff!' do
    context 'with SMB1' do
      let(:raw_response) { double('Raw response') }
      let(:logoff_response) {
        RubySMB::SMB1::Packet::LogoffResponse.new(smb_header: {:command => RubySMB::SMB1::Commands::SMB_COM_LOGOFF} )
      }
      before :example do
        allow(smb1_client).to receive(:send_recv).and_return(raw_response)
        allow(RubySMB::SMB1::Packet::LogoffResponse).to receive(:read).and_return(logoff_response)
        allow(smb1_client).to receive(:wipe_state!)
      end

      it 'creates a LogoffRequest packet' do
        expect(RubySMB::SMB1::Packet::LogoffRequest).to receive(:new).and_call_original
        smb1_client.logoff!
      end

      it 'calls #send_recv' do
        expect(smb1_client).to receive(:send_recv)
        smb1_client.logoff!
      end

      it 'reads the raw response as a LogoffResponse packet' do
        expect(RubySMB::SMB1::Packet::LogoffResponse).to receive(:read).with(raw_response)
        smb1_client.logoff!
      end

      it 'raise an InvalidPacket exception when the response is an empty packet' do
        allow(RubySMB::SMB1::Packet::LogoffResponse).to receive(:read).and_return(RubySMB::SMB1::Packet::EmptyPacket.new)
        expect {smb1_client.logoff!}.to raise_error(RubySMB::Error::InvalidPacket)
      end

      it 'raise an InvalidPacket exception when the response is not valid' do
        allow(logoff_response).to receive(:valid?).and_return(false)
        expect {smb1_client.logoff!}.to raise_error(RubySMB::Error::InvalidPacket)
      end

      it 'calls #wipe_state!' do
        expect(smb1_client).to receive(:wipe_state!)
        smb1_client.logoff!
      end

      it 'returns the expected status code' do
        logoff_response.smb_header.nt_status = WindowsError::NTStatus::STATUS_PENDING.value
        allow(RubySMB::SMB1::Packet::LogoffResponse).to receive(:read).and_return(logoff_response)
        expect(smb1_client.logoff!).to eq(WindowsError::NTStatus::STATUS_PENDING)
      end
    end

    context 'with SMB2' do
      let(:raw_response) { double('Raw response') }
      let(:logoff_response) {
        RubySMB::SMB2::Packet::LogoffResponse.new(smb_header: {:command => RubySMB::SMB2::Commands::LOGOFF} )
      }
      before :example do
        allow(smb2_client).to receive(:send_recv).and_return(raw_response)
        allow(RubySMB::SMB2::Packet::LogoffResponse).to receive(:read).and_return(logoff_response)
        allow(smb2_client).to receive(:wipe_state!)
      end

      it 'creates a LogoffRequest packet' do
        expect(RubySMB::SMB2::Packet::LogoffRequest).to receive(:new).and_call_original
        smb2_client.logoff!
      end

      it 'calls #send_recv' do
        expect(smb2_client).to receive(:send_recv)
        smb2_client.logoff!
      end

      it 'reads the raw response as a LogoffResponse packet' do
        expect(RubySMB::SMB2::Packet::LogoffResponse).to receive(:read).with(raw_response)
        smb2_client.logoff!
      end

      it 'raise an InvalidPacket exception when the response is an error packet' do
        allow(RubySMB::SMB2::Packet::LogoffResponse).to receive(:read).and_return(RubySMB::SMB2::Packet::ErrorPacket.new)
        expect {smb2_client.logoff!}.to raise_error(RubySMB::Error::InvalidPacket)
      end

      it 'raise an InvalidPacket exception when the response is not a LOGOFF command' do
        logoff_response.smb2_header.command = RubySMB::SMB2::Commands::ECHO
        allow(RubySMB::SMB2::Packet::LogoffResponse).to receive(:read).and_return(logoff_response)
        expect {smb2_client.logoff!}.to raise_error(RubySMB::Error::InvalidPacket)
      end
    end

    context 'with SMB3' do
      let(:raw_response) { double('Raw response') }
      let(:logoff_response) {
        RubySMB::SMB2::Packet::LogoffResponse.new(smb_header: {:command => RubySMB::SMB2::Commands::LOGOFF} )
      }
      before :example do
        allow(smb3_client).to receive(:send_recv).and_return(raw_response)
        allow(RubySMB::SMB2::Packet::LogoffResponse).to receive(:read).and_return(logoff_response)
        allow(smb3_client).to receive(:wipe_state!)
      end

      it 'creates a LogoffRequest packet' do
        expect(RubySMB::SMB2::Packet::LogoffRequest).to receive(:new).and_call_original
        smb3_client.logoff!
      end

      it 'calls #send_recv' do
        expect(smb3_client).to receive(:send_recv)
        smb3_client.logoff!
      end

      it 'reads the raw response as a LogoffResponse packet' do
        expect(RubySMB::SMB2::Packet::LogoffResponse).to receive(:read).with(raw_response)
        smb3_client.logoff!
      end

      it 'raise an InvalidPacket exception when the response is an error packet' do
        allow(RubySMB::SMB2::Packet::LogoffResponse).to receive(:read).and_return(RubySMB::SMB2::Packet::ErrorPacket.new)
        expect {smb3_client.logoff!}.to raise_error(RubySMB::Error::InvalidPacket)
      end

      it 'raise an InvalidPacket exception when the response is not a LOGOFF command' do
        logoff_response.smb2_header.command = RubySMB::SMB2::Commands::ECHO
        allow(RubySMB::SMB2::Packet::LogoffResponse).to receive(:read).and_return(logoff_response)
        expect {smb3_client.logoff!}.to raise_error(RubySMB::Error::InvalidPacket)
      end
    end
  end

  context 'NetBIOS Session Service' do
    describe '#session_request' do
      let(:session_header)  { RubySMB::Nbss::SessionHeader.new }
      let(:session_request) { RubySMB::Nbss::SessionRequest.new }

      before :example do
        allow(RubySMB::Nbss::SessionRequest).to receive(:new).and_return(session_request)
        allow(dispatcher).to receive(:send_packet)
        allow(dispatcher).to receive(:recv_packet).and_return(session_header.to_binary_s)
      end

      it 'calls #session_request_packet' do
        called_name = 'SPECNAME'
        expect(client).to receive(:session_request_packet).with(called_name)
        client.session_request(called_name)
      end

      it 'sends the SessionRequest packet without adding additional NetBIOS Session Header' do
        expect(dispatcher).to receive(:send_packet).with(session_request, nbss_header: false)
        client.session_request
      end

      it 'reads the full response packet, including the NetBIOS Session Header' do
        expect(dispatcher).to receive(:recv_packet).with(full_response: true).and_return(session_header.to_binary_s)
        client.session_request
      end

      it 'parses the response with SessionHeader packet structure' do
        expect(RubySMB::Nbss::SessionHeader).to receive(:read).with(session_header.to_binary_s).and_return(session_header)
        client.session_request
      end

      it 'returns true when it is a POSITIVE_SESSION_RESPONSE' do
        session_header.session_packet_type = RubySMB::Nbss::POSITIVE_SESSION_RESPONSE
        expect(client.session_request).to be true
      end

      it 'raises an exception when it is a NEGATIVE_SESSION_RESPONSE' do
        negative_session_response = RubySMB::Nbss::NegativeSessionResponse.new
        negative_session_response.session_header.session_packet_type = RubySMB::Nbss::NEGATIVE_SESSION_RESPONSE
        negative_session_response.error_code = 0x80
        allow(dispatcher).to receive(:recv_packet).and_return(negative_session_response.to_binary_s)
        expect { client.session_request }.to raise_error(RubySMB::Error::NetBiosSessionService)
      end

      it 'raises an InvalidPacket exception when an error occurs while reading' do
        allow(RubySMB::Nbss::SessionHeader).to receive(:read).and_raise(IOError)
        expect { client.session_request }.to raise_error(RubySMB::Error::InvalidPacket)
      end
    end

    describe '#session_request_packet' do
      it 'creates a SessionRequest packet' do
        session_request = RubySMB::Nbss::SessionRequest.new
        expect(RubySMB::Nbss::SessionRequest).to receive(:new).and_return(session_request)
        client.session_request_packet
      end

      it 'sets the expected fields of the SessionRequest packet' do
        name         = 'NBNAMESPEC'
        called_name  = 'NBNAMESPEC      '
        calling_name = "               \x00"

        session_packet = client.session_request_packet(name)
        expect(session_packet).to be_a(RubySMB::Nbss::SessionRequest)
        expect(session_packet.session_header.session_packet_type).to eq RubySMB::Nbss::SESSION_REQUEST
        expect(session_packet.called_name).to eq called_name
        expect(session_packet.calling_name).to eq calling_name
        expect(session_packet.session_header.stream_protocol_length).to eq(
          session_packet.called_name.to_binary_s.size + session_packet.calling_name.to_binary_s.size
        )
      end

      it 'converts the called name to upperase' do
        name = 'myname'
        session_packet = client.session_request_packet(name)
        expect(session_packet.called_name).to eq("#{name.upcase.ljust(15)}\x20")
      end

      it 'returns a session packet with *SMBSERVER by default' do
        expect(client.session_request_packet.called_name).to eq('*SMBSERVER      ')
      end
    end
  end

  context 'Protocol Negotiation' do
    let(:random_junk) { 'fgrgrwgawrtw4t4tg4gahgn' }
    let(:smb1_capabilities) {
      { level_2_oplocks: 1,
        nt_status: 1,
        rpc_remote_apis: 1,
        nt_smbs: 1,
        large_files: 1,
        unicode: 1,
        mpx_mode: 0,
        raw_mode: 0,
        large_writex: 1,
        large_readx: 1,
        info_level_passthru: 1,
        dfs: 0,
        reserved1: 0,
        bulk_transfer: 0,
        nt_find: 1,
        lock_and_read: 1,
        unix: 0,
        reserved2: 0,
        lwio: 1,
        extended_security: 1,
        reserved3: 0,
        dynamic_reauth: 0,
        reserved4: 0,
        compressed_data: 0,
        reserved5: 0 }
    }
    let(:smb1_extended_response) {
      packet = RubySMB::SMB1::Packet::NegotiateResponseExtended.new
      packet.parameter_block.capabilities = smb1_capabilities
      packet
    }
    let(:smb1_extended_response_raw) {
      smb1_extended_response.to_binary_s
    }

    let(:smb1_response) {
      packet = RubySMB::SMB1::Packet::NegotiateResponse.new
      smb1_capabilities_dup = smb1_capabilities.dup
      smb1_capabilities_dup[:extended_security] = 0

      packet.parameter_block.capabilities = smb1_capabilities_dup
      packet
    }
    let(:smb1_response_raw) {
      smb1_response.to_binary_s
    }

    let(:smb2_response) { RubySMB::SMB2::Packet::NegotiateResponse.new(dialect_revision: 0x200) }
    let(:smb3_response) { RubySMB::SMB2::Packet::NegotiateResponse.new(dialect_revision: 0x300) }

    describe '#smb1_negotiate_request' do
      it 'returns an SMB1 Negotiate Request packet' do
        expect(client.smb1_negotiate_request).to be_a(RubySMB::SMB1::Packet::NegotiateRequest)
      end

      it 'sets the default SMB1 Dialect' do
        expect(client.smb1_negotiate_request.dialects).to include(
          buffer_format: 2,
          dialect_string: RubySMB::Client::SMB1_DIALECT_SMB1_DEFAULT
        )
      end

      it 'sets the SMB2.02 dialect if SMB2 support is enabled' do
        expect(client.smb1_negotiate_request.dialects).to include(
          buffer_format: 2,
          dialect_string: RubySMB::Client::SMB1_DIALECT_SMB2_DEFAULT
        )
      end

      it 'excludes the SMB2.02 Dialect if SMB2 support is disabled' do
        expect(smb1_client.smb1_negotiate_request.dialects).to_not include(
          buffer_format: 2,
          dialect_string: RubySMB::Client::SMB1_DIALECT_SMB2_DEFAULT
        )
      end

      it 'excludes the default SMB1 Dialect if SMB1 support is disabled' do
        expect(smb2_client.smb1_negotiate_request.dialects).to_not include(
          buffer_format: 2,
          dialect_string: RubySMB::Client::SMB1_DIALECT_SMB1_DEFAULT
        )
      end

      it 'sets the SMB wildcard dialect if SMB2 support is enabled' do
        expect(client.smb1_negotiate_request.dialects).to include(
          buffer_format: 2,
          dialect_string: RubySMB::Client::SMB1_DIALECT_SMB2_WILDCARD
        )
      end

      it 'sets the SMB wildcard dialect if SMB3 support is enabled' do
        expect(smb3_client.smb1_negotiate_request.dialects).to include(
          buffer_format: 2,
          dialect_string: RubySMB::Client::SMB1_DIALECT_SMB2_WILDCARD
        )
      end

      it 'excludes the SMB wildcard dialect if both SMB2 and SMB3 supports are disabled' do
        expect(smb1_client.smb1_negotiate_request.dialects).to_not include(
          buffer_format: 2,
          dialect_string: RubySMB::Client::SMB1_DIALECT_SMB2_WILDCARD
        )
      end
    end

    describe '#smb2_3_negotiate_request' do
      it 'return an SMB2 Negotiate Request packet' do
        expect(client.smb2_3_negotiate_request).to be_a(RubySMB::SMB2::Packet::NegotiateRequest)
      end

      it 'sets the default SMB2 Dialect if SMB2 support is enabled' do
        expect(client.smb2_3_negotiate_request.dialects).to include(
          *(RubySMB::Client::SMB2_DIALECT_DEFAULT.map {|d| d.to_i(16)})
        )
      end

      it 'does not set the default SMB2 Dialect if SMB2 support is disabled' do
        expect(smb3_client.smb2_3_negotiate_request.dialects).to_not include(
          *(RubySMB::Client::SMB2_DIALECT_DEFAULT.map {|d| d.to_i(16)})
        )
      end

      it 'sets the Message ID to 0' do
        expect(client.smb2_3_negotiate_request.smb2_header.message_id).to eq 0
      end

      it 'adds SMB3 dialects if if SMB3 support is enabled' do
        expect(client.smb2_3_negotiate_request.dialects).to include(
          *(RubySMB::Client::SMB3_DIALECT_DEFAULT.map {|d| d.to_i(16)})
        )
      end

      it 'does not set the default SMB3 Dialect if SMB3 support is disabled' do
        expect(smb2_client.smb2_3_negotiate_request.dialects).to_not include(
          *(RubySMB::Client::SMB3_DIALECT_DEFAULT.map {|d| d.to_i(16)})
        )
      end
    end

    describe '#add_smb3_to_negotiate_request' do
      let(:negotiate_request) { RubySMB::SMB2::Packet::NegotiateRequest.new }

      it 'adds the default SMB3 dialects' do
        expect(client.add_smb3_to_negotiate_request(negotiate_request).dialects).to include(
          *(RubySMB::Client::SMB3_DIALECT_DEFAULT.map {|d| d.to_i(16)})
        )
      end

      it 'raises the expected exception when the dialects is not an array of strings' do
        dialects = ['0x0300', 0x0302, '0x0311']
        expect { client.add_smb3_to_negotiate_request(negotiate_request, dialects) }.to raise_error(ArgumentError)
      end

      it 'sets encryption capability flag' do
        expect(client.add_smb3_to_negotiate_request(negotiate_request).capabilities.encryption).to eq(1)
      end

      context 'when the negotiate packet includes the 0x0311 dialect' do
        before :example do
          client.add_smb3_to_negotiate_request(negotiate_request, ['0x0311'])
        end

        it 'adds 3 Negotiate Contexts' do
          expect(negotiate_request.negotiate_context_info.negotiate_context_count).to eq(3)
        end

        it 'adds a Preauth Integrity Negotiate Context with the expected hash algorithms' do
          nc = negotiate_request.negotiate_context_list.select do |n|
              n.context_type == RubySMB::SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES
          end
          expect(nc.length).to eq(1)
          expect(nc.first.data.hash_algorithms).to eq([RubySMB::SMB2::PreauthIntegrityCapabilities::SHA_512])
        end

        it 'adds Encryption Negotiate Contexts with the expected encryption algorithms' do
          nc = negotiate_request.negotiate_context_list.select do |n|
              n.context_type == RubySMB::SMB2::NegotiateContext::SMB2_ENCRYPTION_CAPABILITIES
          end
          expect(nc.length).to eq(1)
          expect(nc.first.data.ciphers).to eq(
            [
              RubySMB::SMB2::EncryptionCapabilities::AES_256_GCM,
              RubySMB::SMB2::EncryptionCapabilities::AES_256_CCM,
              RubySMB::SMB2::EncryptionCapabilities::AES_128_GCM,
              RubySMB::SMB2::EncryptionCapabilities::AES_128_CCM
            ]
          )
        end

        it 'adds Compression Negotiate Contexts with the expected compression algorithms' do
          nc = negotiate_request.negotiate_context_list.select do |n|
              n.context_type == RubySMB::SMB2::NegotiateContext::SMB2_COMPRESSION_CAPABILITIES
          end
          expect(nc.length).to eq(1)
          expect(nc.first.data.compression_algorithms).to eq(
            [
              RubySMB::SMB2::CompressionCapabilities::LZNT1,
              RubySMB::SMB2::CompressionCapabilities::LZ77,
              RubySMB::SMB2::CompressionCapabilities::LZ77_Huffman,
              RubySMB::SMB2::CompressionCapabilities::Pattern_V1
            ]
          )
        end
      end

      context 'when the negotiate packet does not include the 0x0311 dialect' do
        it 'does not add any Negotiate Context' do
          client.add_smb3_to_negotiate_request(negotiate_request, ['0x0300', '0x0302'])
          expect(negotiate_request.negotiate_context_list?). to be false
        end
      end
    end

    describe '#negotiate_request' do
      it 'calls #smb1_negotiate_request if SMB1 is enabled' do
        expect(smb1_client).to receive(:smb1_negotiate_request)
        smb1_client.negotiate_request
      end

      it 'calls #smb1_negotiate_request if both protocols are enabled' do
        expect(client).to receive(:smb1_negotiate_request)
        client.negotiate_request
      end

      it 'calls #smb2_3_negotiate_request if SMB2 is enabled' do
        expect(smb2_client).to receive(:smb2_3_negotiate_request)
        smb2_client.negotiate_request
      end

      it 'calls #smb2_3_negotiate_request if SMB3 is enabled' do
        expect(smb3_client).to receive(:smb2_3_negotiate_request)
        smb3_client.negotiate_request
      end
    end

    describe '#negotiate_response' do
      context 'with only SMB1' do
        it 'returns a properly formed NegotiateResponseExtended packet if extended_security is set as 1' do
          expect(smb1_client.negotiate_response(smb1_extended_response_raw)).to eq smb1_extended_response
        end

        it 'returns a properly formed NegotiateResponse packet if extended_security is set as 0' do
          expect(smb1_client.negotiate_response(smb1_response_raw)).to eq smb1_response
        end

        it 'raises an exception if the response is not a SMB packet' do
          expect { smb1_client.negotiate_response(random_junk) }.to raise_error(RubySMB::Error::InvalidPacket)
        end

        it 'raises an InvalidPacket error if the response is not a valid response' do
          empty_packet.smb_header.command = RubySMB::SMB2::Commands::NEGOTIATE
          expect { smb1_client.negotiate_response(empty_packet.to_binary_s) }.to raise_error(RubySMB::Error::InvalidPacket)
        end

        it 'considers the response invalid if it is not an actual Negotiate Response' do
          bogus_response = smb1_extended_response
          bogus_response.smb_header.command = 0xff
          expect { smb1_client.negotiate_response(bogus_response.to_binary_s) }.to raise_error(RubySMB::Error::InvalidPacket)
        end
      end

      context 'with only SMB2' do
        it 'returns a properly formed packet' do
          expect(smb2_client.negotiate_response(smb2_response.to_binary_s)).to eq smb2_response
        end

        it 'raises an exception if the Response is invalid' do
          expect { smb2_client.negotiate_response(random_junk) }.to raise_error(RubySMB::Error::InvalidPacket)
        end

        it 'considers the response invalid if it is not an actual Negotiate Response' do
          bogus_response = smb2_response
          bogus_response.smb2_header.command = RubySMB::SMB2::Commands::ECHO
          expect { smb2_client.negotiate_response(bogus_response.to_binary_s) }.to raise_error(RubySMB::Error::InvalidPacket)
        end
      end

      context 'with only SMB3' do
        it 'returns a properly formed packet' do
          expect(smb3_client.negotiate_response(smb2_response.to_binary_s)).to eq smb2_response
        end

        it 'raises an exception if the Response is invalid' do
          expect { smb3_client.negotiate_response(random_junk) }.to raise_error(RubySMB::Error::InvalidPacket)
        end

        it 'considers the response invalid if it is not an actual Negotiate Response' do
          bogus_response = smb2_response
          bogus_response.smb2_header.command = RubySMB::SMB2::Commands::ECHO
          expect { smb3_client.negotiate_response(bogus_response.to_binary_s) }.to raise_error(RubySMB::Error::InvalidPacket)
        end
      end

      context 'with SMB1, SMB2 and SMB3 enabled' do
        it 'returns an SMB1 NegotiateResponse if it looks like SMB1' do
          expect(client.negotiate_response(smb1_extended_response_raw)).to eq smb1_extended_response
        end

        it 'returns an SMB2 NegotiateResponse if it looks like SMB2 or SMB3' do
          expect(client.negotiate_response(smb2_response.to_binary_s)).to eq smb2_response
        end
      end
    end

    describe '#parse_negotiate_response' do
      context 'when SMB1 was Negotiated' do
        it 'turns off SMB2 and SMB3 support' do
          client.parse_negotiate_response(smb1_extended_response)
          expect(client.smb2).to be false
          expect(client.smb3).to be false
        end

        it 'sets whether or not signing is required' do
          smb1_extended_response.parameter_block.security_mode.security_signatures_required = 1
          client.parse_negotiate_response(smb1_extended_response)
          expect(client.signing_required).to be true
        end

        it 'sets #dialect to the negotiated dialect' do
          smb1_extended_response.dialects = [
            RubySMB::SMB1::Dialect.new(dialect_string: 'A'),
            RubySMB::SMB1::Dialect.new(dialect_string: 'B'),
            RubySMB::SMB1::Dialect.new(dialect_string: 'C'),
          ]
          smb1_extended_response.parameter_block.dialect_index = 1
          client.parse_negotiate_response(smb1_extended_response)
          expect(client.dialect).to eq 'B'
        end

        it 'returns the string \'SMB1\'' do
          expect(client.parse_negotiate_response(smb1_extended_response)).to eq ('SMB1')
        end

        it 'sets #negotiated_smb_version to 1' do
          client.parse_negotiate_response(smb1_extended_response)
          expect(client.negotiated_smb_version).to eq(1)
        end
      end

      context 'when SMB2 was negotiated' do
        it 'turns off SMB1 and SMB3 support' do
          client.parse_negotiate_response(smb2_response)
          expect(client.smb1).to be false
          expect(client.smb3).to be false
        end

        it 'sets whether or not signing is required' do
          smb2_response.security_mode.signing_required = 1
          client.parse_negotiate_response(smb2_response)
          expect(client.signing_required).to be true
        end

        it 'sets #dialect to the negotiated dialect' do
          smb2_response.dialect_revision = 2
          client.parse_negotiate_response(smb2_response)
          expect(client.dialect).to eq '0x0002'
        end

        it 'returns the string \'SMB2\'' do
          expect(client.parse_negotiate_response(smb2_response)).to eq ('SMB2')
        end

        it 'sets #server_supports_multi_credit to true when the response has #large_mtu capability set' do
          smb2_response.capabilities.large_mtu = 1
          client.parse_negotiate_response(smb2_response)
          expect(client.server_supports_multi_credit).to be true
        end

        it 'sets #server_supports_multi_credit to false when the dialect is 0x0202' do
          smb2_response.dialect_revision = 0x0202
          smb2_response.capabilities.large_mtu = 1 # just to make sure it won't affect the result
          client.parse_negotiate_response(smb2_response)
          expect(client.server_supports_multi_credit).to be false
        end
      end

      context 'when SMB3 was negotiated' do
        it 'turns off SMB1 and SMB2 support' do
          client.parse_negotiate_response(smb3_response)
          expect(client.smb1).to be false
          expect(client.smb2).to be false
        end

        it 'sets whether or not signing is required' do
          smb3_response.security_mode.signing_required = 1
          client.parse_negotiate_response(smb3_response)
          expect(client.signing_required).to be true
        end

        it 'sets #dialect to the negotiated dialect' do
          client.parse_negotiate_response(smb3_response)
          expect(client.dialect).to eq '0x0300'
        end

        it 'returns the string \'SMB2\'' do
          expect(client.parse_negotiate_response(smb3_response)).to eq ('SMB3')
        end

        it 'sets #server_supports_multi_credit to true when the response has #large_mtu capability set' do
          smb3_response.capabilities.large_mtu = 1
          client.parse_negotiate_response(smb3_response)
          expect(client.server_supports_multi_credit).to be true
        end

        context 'when the server supports encryption' do
          before :example do
            smb3_response.capabilities.encryption = 1
          end

          it 'sets the expected encryption algorithm' do
            client.parse_negotiate_response(smb3_response)
            expect(client.encryption_algorithm).to eq(RubySMB::SMB2::EncryptionCapabilities::ENCRYPTION_ALGORITHM_MAP[RubySMB::SMB2::EncryptionCapabilities::AES_128_CCM])
          end

          it 'keeps session encryption enabled if it was already' do
            client.session_encrypt_data = true
            client.parse_negotiate_response(smb3_response)
            expect(client.session_encrypt_data).to be true
          end

          it 'keeps session encryption disabled if it was already' do
            client.session_encrypt_data = false
            client.parse_negotiate_response(smb3_response)
            expect(client.session_encrypt_data).to be false
          end
        end

        context 'when the server does not support encryption' do
          before :example do
            smb3_response.capabilities.encryption = 0
          end

          it 'disables session encryption if it was already enabled' do
            client.session_encrypt_data = true
            client.parse_negotiate_response(smb3_response)
            expect(client.session_encrypt_data).to be false
          end

          it 'keeps session encryption disabled if it was already' do
            client.session_encrypt_data = false
            client.parse_negotiate_response(smb3_response)
            expect(client.session_encrypt_data).to be false
          end
        end
      end

      context 'when the response contains the SMB2 wildcard revision number dialect' do
        it 'only turns off SMB1 support' do
          smb2_response = RubySMB::SMB2::Packet::NegotiateResponse.new(dialect_revision: 0x02ff)
          client.parse_negotiate_response(smb2_response)
          expect(client.smb1).to be false
          expect(client.smb2).to be true
          expect(client.smb3).to be true
        end
      end

      context 'when the negotiation failed' do
        context 'with a STATUS_NOT_SUPPORTED status code' do
          before :example do
            error_packet.smb2_header.nt_status = WindowsError::NTStatus::STATUS_NOT_SUPPORTED.value
          end

          it 'raises the expected exception with SMB2' do
            expect { smb2_client.parse_negotiate_response(error_packet) }.to raise_error(
            RubySMB::Error::NegotiationFailure,
            'Unable to negotiate with remote host, SMB2 not supported'
            )
          end

          it 'raises the expected exception with SMB3' do
            expect { smb3_client.parse_negotiate_response(error_packet) }.to raise_error(
            RubySMB::Error::NegotiationFailure,
            'Unable to negotiate with remote host, SMB3 not supported'
            )
          end
        end

        context 'with an unknown status code' do
          it 'raises the expected exception' do
            expect { client.parse_negotiate_response(empty_packet) }.to raise_error(
            RubySMB::Error::NegotiationFailure,
            'Unable to negotiate with remote host'
            )
          end
        end
      end
    end

    describe '#negotiate' do
      let(:request_packet) { client.smb1_negotiate_request }
      before :example do
        allow(client).to receive(:negotiate_request)
        allow(client).to receive(:send_recv)
        allow(client).to receive(:negotiate_response)
        allow(client).to receive(:parse_negotiate_response)
      end

      it 'calls the backing methods' do
        request_packet = double('Request packet')
        allow(client).to receive(:negotiate_request).and_return(request_packet)
        allow(request_packet).to receive(:packet_smb_version)
        expect(client).to receive(:negotiate_request)
        expect(client).to receive(:send_recv)
        expect(client).to receive(:negotiate_response)
        expect(client).to receive(:parse_negotiate_response)
        client.negotiate
      end

      context 'with SMB1' do
        it 'sets the response-packet #dialects array with the dialects sent in the request' do
          request_packet = client.smb1_negotiate_request
          allow(client).to receive(:negotiate_request).and_return(request_packet)
          allow(client).to receive(:negotiate_response).and_return(smb1_extended_response)
          expect(smb1_extended_response).to receive(:dialects=).with(request_packet.dialects)
          client.negotiate
        end
      end

      context "with 0x0311 dialect" do
        it 'calls #parse_negotiate_response and updates the preauth hash' do
          client.dialect = '0x0311'
          request_packet = client.smb2_3_negotiate_request
          allow(client).to receive(:negotiate_request).and_return(request_packet)
          allow(client).to receive(:negotiate_response).and_return(smb3_response)
          expect(client).to receive(:parse_negotiate_response).with(smb3_response)
          expect(client).to receive(:update_preauth_hash).with(request_packet)
          expect(client).to receive(:update_preauth_hash).with(smb3_response)
          client.negotiate
        end
      end

      context 'with a wildcard revision number response' do
        before :example do
          client.dialect = '0x02ff'
          allow(client).to receive(:smb2_message_id=) do
            client.dialect = '0x0202'
          end
        end

        it 'increments the message ID' do
          expect(client).to receive(:smb2_message_id=).with(1)
          expect(client).to receive(:negotiate_request).twice.and_call_original
          expect(client).to receive(:parse_negotiate_response).twice do
            client.smb1 = false
          end
          client.negotiate
        end

        it 're-negotiates' do
          expect(client).to receive(:negotiate_request).twice.and_call_original
          expect(client).to receive(:send_recv).twice
          expect(client).to receive(:negotiate_response).twice
          expect(client).to receive(:parse_negotiate_response).twice do
            client.smb1 = false
          end
          client.negotiate
        end
      end

      context 'when an error occurs' do
        before :example do
          allow(client).to receive(:negotiate_request).and_return(request_packet)
          allow(client).to receive(:send_recv).and_raise(RubySMB::Error::InvalidPacket)
          client.smb1 = false
          client.smb2 = false
          client.smb3 = false
        end

        context 'with SMB1' do
          let(:request_packet) { client.smb1_negotiate_request }

          it 'raise the expected exception' do
            client.smb1 = true
            expect { client.negotiate }.to raise_error(
              RubySMB::Error::NegotiationFailure,
              "Unable to negotiate SMB1 with the remote host: RubySMB::Error::InvalidPacket"
            )
          end
        end

        context 'with SMB2' do
          let(:request_packet) { client.smb2_3_negotiate_request }

          it 'raise the expected exception' do
            client.smb2 = true
            expect { client.negotiate }.to raise_error(
              RubySMB::Error::NegotiationFailure,
              "Unable to negotiate SMB2 with the remote host: RubySMB::Error::InvalidPacket"
            )
          end
        end

        context 'with SMB3' do
          let(:request_packet) { client.smb2_3_negotiate_request }

          it 'raise the expected exception' do
            client.smb3 = true
            expect { client.negotiate }.to raise_error(
              RubySMB::Error::NegotiationFailure,
              "Unable to negotiate SMB3 with the remote host: RubySMB::Error::InvalidPacket"
            )
          end
        end
      end

      describe '#parse_smb3_capabilities' do
        let(:request_packet) { client.smb2_3_negotiate_request }
        let(:smb3_response) { RubySMB::SMB2::Packet::NegotiateResponse.new(dialect_revision: 0x311) }
        let(:nc_encryption) do
          nc = RubySMB::SMB2::NegotiateContext.new(
            context_type: RubySMB::SMB2::NegotiateContext::SMB2_ENCRYPTION_CAPABILITIES
          )
          nc.data.ciphers << RubySMB::SMB2::EncryptionCapabilities::AES_128_CCM
          nc
        end
        let(:nc_integrity) do
          nc = RubySMB::SMB2::NegotiateContext.new(
            context_type: RubySMB::SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES
          )
          nc.data.hash_algorithms << RubySMB::SMB2::PreauthIntegrityCapabilities::SHA_512
          nc
        end

        before :example do
          allow(smb3_client).to receive(:update_preauth_hash)
          smb3_response.add_negotiate_context(nc_encryption)
          smb3_response.add_negotiate_context(nc_integrity)
        end

        context 'when selecting the integrity hash algorithm' do
          context 'with one algorithm' do
            it 'selects the expected algorithm' do
              smb3_client.parse_smb3_capabilities(smb3_response)
              expect(smb3_client.preauth_integrity_hash_algorithm).to eq('SHA512')
            end
          end

          context 'with multiple algorithms' do
            it 'selects the first algorithm' do
              nc = smb3_response.find_negotiate_context(
                RubySMB::SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES
              )
              nc.data.hash_algorithms << 3
              smb3_client.parse_smb3_capabilities(smb3_response)
              expect(smb3_client.preauth_integrity_hash_algorithm).to eq('SHA512')
            end
          end

          context 'without integrity negotiate context' do
            it 'raises the expected exception' do
              smb3_response = RubySMB::SMB2::Packet::NegotiateResponse.new(dialect_revision: 0x311)
              smb3_response.add_negotiate_context(nc_encryption)
              expect { smb3_client.parse_smb3_capabilities(smb3_response) }.to raise_error(
                RubySMB::Error::EncryptionError,
                'Unable to retrieve the Preauth Integrity Hash Algorithm from the Negotiate response'
              )
            end
          end

          context 'with an unknown integrity hash algorithm' do
            it 'raises the expected exception' do
              smb3_response = RubySMB::SMB2::Packet::NegotiateResponse.new(dialect_revision: 0x311)
              smb3_response.add_negotiate_context(nc_encryption)
              nc = RubySMB::SMB2::NegotiateContext.new(
                context_type: RubySMB::SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES
              )
              nc.data.hash_algorithms << 5
              smb3_response.add_negotiate_context(nc)
              expect { smb3_client.parse_smb3_capabilities(smb3_response) }.to raise_error(
                RubySMB::Error::EncryptionError,
                'Unable to retrieve the Preauth Integrity Hash Algorithm from the Negotiate response'
              )
            end
          end
        end

        context 'when selecting the encryption algorithm' do
          context 'with one algorithm' do
            it 'selects the expected algorithm' do
              smb3_client.parse_smb3_capabilities(smb3_response)
              expect(smb3_client.encryption_algorithm).to eq('AES-128-CCM')
            end
          end

          context 'with multiple algorithms' do
            it 'selects the AES-128-GCM algorithm if included' do
              nc = smb3_response.find_negotiate_context(
                RubySMB::SMB2::NegotiateContext::SMB2_ENCRYPTION_CAPABILITIES
              )
              nc.data.ciphers << RubySMB::SMB2::EncryptionCapabilities::AES_128_GCM
              smb3_client.parse_smb3_capabilities(smb3_response)
              expect(smb3_client.encryption_algorithm).to eq('AES-128-GCM')
            end

            it 'selects the first algorithm if AES-128-GCM is not included' do
              nc = smb3_response.find_negotiate_context(
                RubySMB::SMB2::NegotiateContext::SMB2_ENCRYPTION_CAPABILITIES
              )
              nc.data.ciphers << 3
              smb3_client.parse_smb3_capabilities(smb3_response)
              expect(smb3_client.encryption_algorithm).to eq('AES-128-CCM')
            end

            it 'keep tracks of the server supported algorithms' do
              nc = smb3_response.find_negotiate_context(
                RubySMB::SMB2::NegotiateContext::SMB2_ENCRYPTION_CAPABILITIES
              )
              nc.data.ciphers << RubySMB::SMB2::EncryptionCapabilities::AES_128_GCM
              smb3_client.parse_smb3_capabilities(smb3_response)
              expect(smb3_client.server_encryption_algorithms).to eq([1, 2])
            end
          end

          context 'without encryption context' do
            it 'raises the expected exception' do
              smb3_response = RubySMB::SMB2::Packet::NegotiateResponse.new(dialect_revision: 0x311)
              smb3_response.add_negotiate_context(nc_integrity)
              expect { smb3_client.parse_smb3_capabilities(smb3_response) }.to raise_error(
                RubySMB::Error::EncryptionError,
                'Unable to retrieve the encryption cipher list supported by the server from the Negotiate response'
              )
            end
          end

          context 'with an unknown encryption algorithm' do
            it 'raises the expected exception' do
              smb3_response = RubySMB::SMB2::Packet::NegotiateResponse.new(dialect_revision: 0x311)
              smb3_response.add_negotiate_context(nc_integrity)
              nc = RubySMB::SMB2::NegotiateContext.new(
                context_type: RubySMB::SMB2::NegotiateContext::SMB2_ENCRYPTION_CAPABILITIES
              )
              nc.data.ciphers << 14
              smb3_response.add_negotiate_context(nc)
              expect { smb3_client.parse_smb3_capabilities(smb3_response) }.to raise_error(
                RubySMB::Error::EncryptionError,
                'Unable to retrieve the encryption cipher list supported by the server from the Negotiate response'
              )
            end
          end
        end

        context 'when selecting the compression algorithm' do
          it 'keep tracks of the server supported algorithms' do
            nc = RubySMB::SMB2::NegotiateContext.new(
              context_type: RubySMB::SMB2::NegotiateContext::SMB2_COMPRESSION_CAPABILITIES
            )
            nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::LZNT1
            nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::LZ77
            nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::LZ77_Huffman
            nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::Pattern_V1
            smb3_response.add_negotiate_context(nc)
            smb3_client.parse_smb3_capabilities(smb3_response)
            expect(smb3_client.server_compression_algorithms).to eq([1, 2, 3, 4])
          end
        end
      end
    end
  end

  context 'Authentication' do
    let(:type2_string) {
      "TlRMTVNTUAACAAAAHgAeADgAAAA1goriwmZ8HEHtFHAAAAAAAAAAAJgAmABW\nAAAABgGxHQAAAA" \
        "9XAEkATgAtAFMATgBKAEQARwAwAFUAQQA5ADAARgACAB4A\nVwBJAE4ALQBTAE4ASgBEAEcAMABV" \
        "AEEAOQAwAEYAAQAeAFcASQBOAC0AUwBO\nAEoARABHADAAVQBBADkAMABGAAQAHgBXAEkATgAtAF" \
        "MATgBKAEQARwAwAFUA\nQQA5ADAARgADAB4AVwBJAE4ALQBTAE4ASgBEAEcAMABVAEEAOQAwAEYABw" \
        "AI\nADxThZ4nnNIBAAAAAA==\n"
    }

    describe '#authenticate' do
      it 'calls #smb2_authenticate if SMB2 was selected/negotiated' do
        expect(smb2_client).to receive(:smb2_authenticate)
        smb2_client.authenticate
      end

      it 'calls #smb1_authenticate if SMB1 was selected and we have credentials' do
        expect(smb1_client).to receive(:smb1_authenticate)
        smb1_client.authenticate
      end

      it 'calls #smb1_anonymous_auth if using SMB1 and no credentials were supplied' do
        smb1_client.username = ''
        smb1_client.password = ''
        expect(smb1_client).to receive(:smb1_anonymous_auth)
        smb1_client.authenticate
      end
    end

    context 'for SMB1' do
      let(:ntlm_client) { smb1_client.ntlm_client }
      let(:type1_message) { ntlm_client.init_context }
      let(:negotiate_packet) { RubySMB::SMB1::Packet::SessionSetupRequest.new }
      let(:response_packet) { RubySMB::SMB1::Packet::SessionSetupResponse.new }
      let(:final_response_packet) { RubySMB::SMB1::Packet::SessionSetupResponse.new }
      let(:type3_message) { ntlm_client.init_context(type2_string) }
      let(:user_id) { 2041 }

      describe '#smb1_authenticate' do
        before :example do
          allow(smb1_client).to receive(:smb1_ntlmssp_negotiate)
          allow(smb1_client).to receive(:smb1_ntlmssp_challenge_packet).and_return(response_packet)
          allow(smb1_client).to receive(:smb1_type2_message).and_return(type2_string)
          allow(smb1_client).to receive(:smb1_ntlmssp_authenticate)
          allow(smb1_client).to receive(:smb1_ntlmssp_final_packet).and_return(final_response_packet)
        end

        it 'calls the backing methods' do
          response_packet.smb_header.uid = user_id
          expect(smb1_client).to receive(:smb1_ntlmssp_negotiate).and_return(negotiate_packet)
          expect(smb1_client).to receive(:smb1_ntlmssp_challenge_packet).with(negotiate_packet).and_return(response_packet)
          expect(smb1_client).to receive(:smb1_type2_message).with(response_packet).and_return(type2_string)
          expect(smb1_client).to receive(:store_target_info).with(String)
          expect(smb1_client).to receive(:extract_os_version).with(String)
          expect(smb1_client).to receive(:smb1_ntlmssp_authenticate).with(Net::NTLM::Message::Type3, user_id)
          expect(smb1_client).to receive(:smb1_ntlmssp_final_packet).and_return(final_response_packet)
          smb1_client.smb1_authenticate
        end

        it 'stores the OS information from the challenge packet' do
          native_os = 'Windows 7 Professional 7601 Service Pack 1'
          native_lm = 'Windows 7 Professional 6.1'
          response_packet.data_block.native_os = native_os
          response_packet.data_block.native_lan_man = native_lm
          smb1_client.smb1_authenticate

          expect(smb1_client.peer_native_os).to eq native_os
          expect(smb1_client.peer_native_lm).to eq native_lm
        end

        it 'stores the session key from the NTLM client' do
          smb1_client.smb1_authenticate
          expect(smb1_client.session_key).to eq ntlm_client.session_key
        end

        it 'stores the OS version number from the challenge message' do
          smb1_client.smb1_authenticate
          expect(smb1_client.os_version).to eq '6.1.7601'
        end

        it 'stores the user ID if the status code is \'STATUS_SUCCESS\'' do
          response_packet.smb_header.uid = user_id
          final_response_packet.smb_header.nt_status = WindowsError::NTStatus::STATUS_SUCCESS.value
          smb1_client.smb1_authenticate
          expect(smb1_client.user_id).to eq user_id
        end

        it 'does not store the user ID if the status code is not \'STATUS_SUCCESS\'' do
          response_packet.smb_header.uid = user_id
          final_response_packet.smb_header.nt_status = WindowsError::NTStatus::STATUS_PENDING.value
          smb1_client.smb1_authenticate
          expect(smb1_client.user_id).to eq nil
        end
      end

      describe '#smb1_ntlmssp_auth_packet' do
        it 'creates a new SessionSetupRequest packet' do
          expect(RubySMB::SMB1::Packet::SessionSetupRequest).to receive(:new).and_return(negotiate_packet)
          smb1_client.smb1_ntlmssp_auth_packet(type3_message, user_id)
        end

        it 'sets the security blob with an NTLM Type 3 Message' do
          expect(RubySMB::SMB1::Packet::SessionSetupRequest).to receive(:new).and_return(negotiate_packet)
          expect(negotiate_packet).to receive(:set_type3_blob).with(type3_message.serialize)
          smb1_client.smb1_ntlmssp_auth_packet(type3_message, user_id)
        end

        it 'enables extended security on the packet' do
          expect(smb1_client.smb1_ntlmssp_auth_packet(type3_message, user_id).smb_header.flags2.extended_security).to eq 1
        end

        it 'sets the max_buffer_size to the client\'s max_buffer_size' do
          expect(smb1_client.smb1_ntlmssp_auth_packet(type3_message, user_id).parameter_block.max_buffer_size).to eq smb1_client.max_buffer_size
        end
      end

      describe '#smb1_ntlmssp_negotiate_packet' do
        it 'creates a new SessionSetupRequest packet' do
          expect(RubySMB::SMB1::Packet::SessionSetupRequest).to receive(:new).and_return(negotiate_packet)
          smb1_client.smb1_ntlmssp_negotiate_packet
        end

        it 'builds the security blob with an NTLM Type 1 Message' do
          expect(RubySMB::SMB1::Packet::SessionSetupRequest).to receive(:new).and_return(negotiate_packet)
          expect(ntlm_client).to receive(:init_context).and_return(type1_message)
          expect(negotiate_packet).to receive(:set_type1_blob).with(type1_message.serialize)
          smb1_client.smb1_ntlmssp_negotiate_packet
        end

        it 'enables extended security on the packet' do
          expect(smb1_client.smb1_ntlmssp_negotiate_packet.smb_header.flags2.extended_security).to eq 1
        end

        it 'sets the max_buffer_size to the client\'s max_buffer_size' do
          expect(smb1_client.smb1_ntlmssp_negotiate_packet.parameter_block.max_buffer_size).to eq smb1_client.max_buffer_size
        end
      end

      describe '#smb1_ntlmssp_authenticate' do
        it 'sends the request packet and receives a response' do
          expect(smb1_client).to receive(:smb1_ntlmssp_auth_packet).and_return(negotiate_packet)
          expect(dispatcher).to receive(:send_packet).with(negotiate_packet)
          expect(dispatcher).to receive(:recv_packet)
          smb1_client.smb1_ntlmssp_authenticate(type3_message, user_id)
        end
      end

      describe '#smb1_ntlmssp_negotiate' do
        it 'sends the request packet and receives a response' do
          expect(smb1_client).to receive(:smb1_ntlmssp_negotiate_packet).and_return(negotiate_packet)
          expect(dispatcher).to receive(:send_packet).with(negotiate_packet)
          expect(dispatcher).to receive(:recv_packet)
          smb1_client.smb1_ntlmssp_negotiate
        end
      end

      describe '#smb1_ntlmssp_challenge_packet' do
        let(:response) {
          packet = RubySMB::SMB1::Packet::SessionSetupResponse.new
          packet.smb_header.nt_status = 0xc0000016
          packet
        }
        let(:wrong_command) {
          packet = RubySMB::SMB1::Packet::SessionSetupResponse.new
          packet.smb_header.nt_status = 0xc0000016
          packet.smb_header.command = RubySMB::SMB1::Commands::SMB_COM_NEGOTIATE
          packet
        }
        it 'returns the packet object' do
          expect(smb1_client.smb1_ntlmssp_challenge_packet(response.to_binary_s)).to eq response
        end

        it 'raises an UnexpectedStatusCode if the status code is not correct' do
          response.smb_header.nt_status = 0xc0000015
          expect { smb1_client.smb1_ntlmssp_challenge_packet(response.to_binary_s) }.to raise_error(RubySMB::Error::UnexpectedStatusCode)
        end

        it 'raise an InvalidPacket exception when the response is not valid' do
          expect { smb1_client.smb1_ntlmssp_challenge_packet(wrong_command.to_binary_s) }.to raise_error(RubySMB::Error::InvalidPacket)
        end
      end

      describe '#smb1_ntlmssp_final_packet' do
        let(:response) {
          packet = RubySMB::SMB1::Packet::SessionSetupResponse.new
          packet.smb_header.nt_status = 0x00000000
          packet
        }
        let(:wrong_command) {
          packet = RubySMB::SMB1::Packet::SessionSetupResponse.new
          packet.smb_header.nt_status = 0x00000000
          packet.smb_header.command = RubySMB::SMB1::Commands::SMB_COM_NEGOTIATE
          packet
        }
        it 'returns the packet object' do
          expect(smb1_client.smb1_ntlmssp_final_packet(response.to_binary_s)).to eq response
        end

        it 'raise an InvalidPacket exception when the response is not valid' do
          expect { smb1_client.smb1_ntlmssp_final_packet(wrong_command.to_binary_s) }.to raise_error(RubySMB::Error::InvalidPacket)
        end
      end

      describe '#smb1_type2_message' do
        let(:fake_type2) { 'NTLMSSP FOO' }
        let(:response_packet) {
          packet = RubySMB::SMB1::Packet::SessionSetupResponse.new
          packet.set_type2_blob(fake_type2)
          packet
        }
        it 'returns a base64 encoded copy of the Type 2 NTLM message' do
          expect(smb1_client.smb1_type2_message(response_packet)).to eq [fake_type2].pack('m')
        end
      end

      describe 'Anonymous Auth' do
        let(:anonymous_request) { RubySMB::SMB1::Packet::SessionSetupLegacyRequest.new }
        let(:anonymous_response) { RubySMB::SMB1::Packet::SessionSetupLegacyResponse.new }

        describe '#smb1_anonymous_auth' do
          it 'calls the backing methods' do
            expect(client).to receive(:smb1_anonymous_auth_request).and_return(anonymous_request)
            expect(client).to receive(:send_recv).with(anonymous_request)
            expect(client).to receive(:smb1_anonymous_auth_response).and_return(anonymous_response)
            client.smb1_anonymous_auth
          end

          it 'returns the status code' do
            allow(client).to receive(:send_recv)
            allow(client).to receive(:smb1_anonymous_auth_response).and_return(anonymous_response)
            anonymous_response.smb_header.nt_status = WindowsError::NTStatus::STATUS_PENDING.value
            expect(client.smb1_anonymous_auth).to eq WindowsError::NTStatus::STATUS_PENDING
          end

          it 'sets the expected Client\'s attribute from the response when the status code is STATUS_SUCCESS' do
            native_os = 'Windows 7 Professional 7601 Service Pack 1'
            native_lm = 'Windows 7 Professional 6.1'
            primary_domain = 'SPEC_DOMAIN'
            anonymous_response.smb_header.uid = user_id
            anonymous_response.data_block.native_os = native_os
            anonymous_response.data_block.native_lan_man = native_lm
            anonymous_response.data_block.primary_domain = primary_domain
            anonymous_response.smb_header.nt_status = WindowsError::NTStatus::STATUS_SUCCESS.value

            allow(client).to receive(:send_recv)
            allow(client).to receive(:smb1_anonymous_auth_response).and_return(anonymous_response)

            client.smb1_anonymous_auth
            expect(client.user_id).to eq user_id
            expect(client.peer_native_os).to eq native_os
            expect(client.peer_native_lm).to eq native_lm
            expect(client.primary_domain).to eq primary_domain
          end
        end

        describe '#smb1_anonymous_auth_request' do
          it 'creates a SessionSetupLegacyRequest packet with a null byte for the oem password' do
            expect(smb1_client.smb1_anonymous_auth_request.data_block.oem_password).to eq "\x00"
          end

          it 'creates a SessionSetupLegacyRequest packet with the max_buffer_size set to the client\'s max_buffer_size' do
            expect(smb1_client.smb1_anonymous_auth_request.parameter_block.max_buffer_size).to eq smb1_client.max_buffer_size
          end
        end

        describe '#smb1_anonymous_auth_response' do
          it 'returns a Legacy Session SetupResponse Packet' do
            expect(smb1_client.smb1_anonymous_auth_response(anonymous_response.to_binary_s)).to eq anonymous_response
          end

          it 'raise an InvalidPacket exception when the response is not valid' do
            anonymous_response.smb_header.command = RubySMB::SMB1::Commands::SMB_COM_NEGOTIATE
            expect { smb1_client.smb1_anonymous_auth_response(anonymous_response.to_binary_s) }.to raise_error(RubySMB::Error::InvalidPacket)
          end
        end
      end
    end

    context 'for SMB2' do
      let(:ntlm_client) { smb2_client.ntlm_client }
      let(:type1_message) { ntlm_client.init_context }
      let(:negotiate_packet) { RubySMB::SMB2::Packet::SessionSetupRequest.new }
      let(:response_packet) { RubySMB::SMB2::Packet::SessionSetupResponse.new }
      let(:final_response_packet) { RubySMB::SMB2::Packet::SessionSetupResponse.new }
      let(:type3_message) { ntlm_client.init_context(type2_string) }
      let(:session_id) { 0x0000040000000005 }

      describe '#smb2_authenticate' do
        before :example do
          allow(smb2_client).to receive(:smb2_ntlmssp_negotiate)
          allow(smb2_client).to receive(:smb2_ntlmssp_challenge_packet).and_return(response_packet)
          allow(smb2_client).to receive(:smb2_type2_message).and_return(type2_string)
          allow(smb2_client).to receive(:smb2_ntlmssp_authenticate)
          allow(smb2_client).to receive(:smb2_ntlmssp_final_packet).and_return(final_response_packet)
        end

        it 'calls the backing methods' do
          response_packet.smb2_header.session_id = session_id
          expect(smb2_client).to receive(:smb2_ntlmssp_negotiate).and_return(negotiate_packet)
          expect(smb2_client).to receive(:smb2_ntlmssp_challenge_packet).with(negotiate_packet).and_return(response_packet)
          expect(smb2_client).to receive(:smb2_type2_message).with(response_packet).and_return(type2_string)
          expect(smb2_client).to receive(:store_target_info).with(String)
          expect(smb2_client).to receive(:extract_os_version).with(String)
          expect(smb2_client).to receive(:smb2_ntlmssp_authenticate).with(Net::NTLM::Message::Type3, session_id)
          expect(smb2_client).to receive(:smb2_ntlmssp_final_packet).and_return(final_response_packet)
          smb2_client.smb2_authenticate
        end

        it 'stores the session ID from the challenge message' do
          response_packet.smb2_header.session_id = session_id
          smb2_client.smb2_authenticate
          expect(smb2_client.session_id).to eq session_id
        end

        it 'stores the session key from the NTLM client' do
          smb2_client.smb2_authenticate
          expect(smb2_client.session_key).to eq ntlm_client.session_key
        end

        it 'stores the OS version number from the challenge message' do
          smb2_client.smb2_authenticate
          expect(smb2_client.os_version).to eq '6.1.7601'
        end

        ['0x0202', '0x0210', '0x0300', '0x0302'].each do |dialect|
          it "does not update the preauth hash with dialect #{dialect}" do
            smb2_client.dialect = dialect
            expect(smb2_client).to_not receive(:update_preauth_hash)
            smb2_client.smb2_authenticate
          end
        end

        it "updates the preauth hash with dialect 0x0311" do
          smb2_client.dialect = '0x0311'
          expect(smb2_client).to receive(:update_preauth_hash).with(response_packet)
          smb2_client.smb2_authenticate
        end

        context 'when setting the session_encrypt_data parameter' do
          before :example do
            smb2_client.smb3 = true
            smb2_client.session_encrypt_data = false
            smb2_client.preauth_integrity_hash_value = ''
          end

          it 'sets the session_encrypt_data parameter to true if the server requires encryption' do
            final_response_packet.session_flags.encrypt_data = 1
            smb2_client.smb2_authenticate
            expect(smb2_client.session_encrypt_data).to be true
          end

          it 'does not set the session_encrypt_data parameter if the server does not require encryption' do
            final_response_packet.session_flags.encrypt_data = 0
            smb2_client.smb2_authenticate
            expect(smb2_client.session_encrypt_data).to be false
          end
        end
      end

      describe '#smb2_ntlmssp_negotiate_packet' do
        it 'creates a new SessionSetupRequest packet' do
          expect(RubySMB::SMB2::Packet::SessionSetupRequest).to receive(:new).and_return(negotiate_packet)
          smb2_client.smb2_ntlmssp_negotiate_packet
        end

        it 'builds the security blob with an NTLM Type 1 Message' do
          expect(RubySMB::SMB2::Packet::SessionSetupRequest).to receive(:new).and_return(negotiate_packet)
          expect(ntlm_client).to receive(:init_context).and_return(type1_message)
          expect(negotiate_packet).to receive(:set_type1_blob).with(type1_message.serialize)
          smb2_client.smb2_ntlmssp_negotiate_packet
        end

        it 'enables signing' do
          expect(smb2_client.smb2_ntlmssp_negotiate_packet.security_mode.signing_enabled).to eq 1
        end
      end

      describe '#smb2_ntlmssp_negotiate' do
        before :example do
          allow(smb2_client).to receive(:smb2_ntlmssp_negotiate_packet).and_return(negotiate_packet)
          allow(smb2_client).to receive(:send_recv)
        end

        it 'sends the request packet and receives a response' do
          expect(smb2_client).to receive(:smb2_ntlmssp_negotiate_packet)
          expect(smb2_client).to receive(:send_recv).with(negotiate_packet)
          smb2_client.smb2_ntlmssp_negotiate
        end

        ['0x0202', '0x0210', '0x0300', '0x0302'].each do |dialect|
          it "does not update the preauth hash with dialect #{dialect}" do
            smb2_client.dialect = dialect
            expect(smb2_client).to_not receive(:update_preauth_hash)
            smb2_client.smb2_ntlmssp_negotiate
          end
        end

        it "updates the preauth hash with dialect 0x0311" do
          smb2_client.dialect = '0x0311'
          expect(smb2_client).to receive(:update_preauth_hash).with(negotiate_packet)
          smb2_client.smb2_ntlmssp_negotiate
        end
      end

      describe '#smb2_ntlmssp_challenge_packet' do
        let(:response) {
          packet = RubySMB::SMB2::Packet::SessionSetupResponse.new
          packet.smb2_header.nt_status = 0xc0000016
          packet
        }
        let(:wrong_command) {
          packet = RubySMB::SMB2::Packet::SessionSetupResponse.new
          packet.smb2_header.nt_status = 0xc0000016
          packet.smb2_header.command = RubySMB::SMB2::Commands::NEGOTIATE
          packet
        }
        it 'returns the packet object' do
          expect(smb2_client.smb2_ntlmssp_challenge_packet(response.to_binary_s)).to eq response
        end

        it 'raises an UnexpectedStatusCode if the status code is not correct' do
          response.smb2_header.nt_status = 0xc0000015
          expect { smb2_client.smb2_ntlmssp_challenge_packet(response.to_binary_s) }.to raise_error(RubySMB::Error::UnexpectedStatusCode)
        end

        it 'raise an InvalidPacket exception when the response is not valid' do
          expect { smb2_client.smb2_ntlmssp_challenge_packet(wrong_command.to_binary_s) }.to raise_error(RubySMB::Error::InvalidPacket)
        end
      end

      describe '#smb2_type2_message' do
        let(:fake_type2) { 'NTLMSSP FOO' }
        let(:response_packet) {
          packet = RubySMB::SMB2::Packet::SessionSetupResponse.new
          packet.set_type2_blob(fake_type2)
          packet
        }
        it 'returns a base64 encoded copy of the Type 2 NTLM message' do
          expect(smb2_client.smb2_type2_message(response_packet)).to eq [fake_type2].pack('m')
        end
      end

      describe '#smb2_ntlmssp_auth_packet' do
        it 'creates a new SessionSetupRequest packet' do
          expect(RubySMB::SMB2::Packet::SessionSetupRequest).to receive(:new).and_return(negotiate_packet)
          smb2_client.smb2_ntlmssp_auth_packet(type3_message, session_id)
        end

        it 'sets the security blob with an NTLM Type 3 Message' do
          expect(RubySMB::SMB2::Packet::SessionSetupRequest).to receive(:new).and_return(negotiate_packet)
          expect(negotiate_packet).to receive(:set_type3_blob).with(type3_message.serialize)
          smb2_client.smb2_ntlmssp_auth_packet(type3_message, session_id)
        end

        it 'sets the session ID on the request packet' do
          expect(smb2_client.smb2_ntlmssp_auth_packet(type3_message, session_id).smb2_header.session_id).to eq session_id
        end

        it 'enables signing' do
          expect(smb2_client.smb2_ntlmssp_auth_packet(type3_message, session_id).security_mode.signing_enabled).to eq 1
        end
      end

      describe '#smb2_ntlmssp_authenticate' do
        before :example do
          allow(smb2_client).to receive(:smb2_ntlmssp_auth_packet).and_return(negotiate_packet)
          allow(smb2_client).to receive(:send_recv)
        end

        it 'sends the request packet and receives a response' do
          expect(smb2_client).to receive(:smb2_ntlmssp_auth_packet)
          expect(smb2_client).to receive(:send_recv).with(negotiate_packet)
          smb2_client.smb2_ntlmssp_authenticate(type3_message, session_id)
        end

        ['0x0202', '0x0210', '0x0300', '0x0302'].each do |dialect|
          it "does not update the preauth hash with dialect #{dialect}" do
            smb2_client.dialect = dialect
            expect(smb2_client).to_not receive(:update_preauth_hash)
            smb2_client.smb2_ntlmssp_authenticate(type3_message, session_id)
          end
        end

        it "updates the preauth hash with dialect 0x0311" do
          smb2_client.dialect = '0x0311'
          expect(smb2_client).to receive(:update_preauth_hash).with(negotiate_packet)
          smb2_client.smb2_ntlmssp_authenticate(type3_message, session_id)
        end
      end

      describe '#smb2_ntlmssp_final_packet' do
        let(:response) {
          packet = RubySMB::SMB2::Packet::SessionSetupResponse.new
          packet.smb2_header.nt_status = 0x00000000
          packet
        }
        let(:wrong_command) {
          packet = RubySMB::SMB2::Packet::SessionSetupResponse.new
          packet.smb2_header.nt_status = 0x00000000
          packet.smb2_header.command = RubySMB::SMB2::Commands::NEGOTIATE
          packet
        }
        it 'returns the packet object' do
          expect(smb2_client.smb2_ntlmssp_final_packet(response.to_binary_s)).to eq response
        end

        it 'raise an InvalidPacket exception when the response is not valid' do
          expect { smb2_client.smb2_ntlmssp_final_packet(wrong_command.to_binary_s) }.to raise_error(RubySMB::Error::InvalidPacket)
        end
      end
    end

    describe '#store_target_info' do
      let(:target_info_str) { "\x02\x00\x14\x00T\x00E\x00S\x00T\x00D\x00O\x00M"\
        "\x00A\x00I\x00N\x00\x01\x00\x10\x00T\x00E\x00S\x00T\x00N\x00A\x00M"\
        "\x00E\x00\x04\x00 \x00t\x00e\x00s\x00t\x00d\x00o\x00m\x00a\x00i\x00"\
        "n\x00.\x00l\x00o\x00c\x00a\x00l\x00\x03\x002\x00t\x00e\x00s\x00t\x00"\
        "n\x00a\x00m\x00e\x00.\x00t\x00e\x00s\x00t\x00d\x00o\x00m\x00a\x00i"\
        "\x00n\x00.\x00l\x00o\x00c\x00a\x00l\x00\x05\x00 \x00t\x00e\x00s\x00t"\
        "\x00f\x00o\x00r\x00e\x00s\x00t\x00.\x00l\x00o\x00c\x00a\x00l\x00\a"\
        "\x00\b\x00Q7w\x01Fh\xD3\x01\x00\x00\x00\x00" }

      it 'creates a Net::NTLM::TargetInfo object from the target_info string' do
        expect(Net::NTLM::TargetInfo).to receive(:new).with(target_info_str).and_call_original
        client.store_target_info(target_info_str)
      end

      it 'sets the expected Client\'s attribute' do
        client.store_target_info(target_info_str)
        expect(client.default_name).to eq 'TESTNAME'
        expect(client.default_domain).to eq 'TESTDOMAIN'
        expect(client.dns_host_name).to eq 'testname.testdomain.local'
        expect(client.dns_domain_name).to eq 'testdomain.local'
        expect(client.dns_tree_name).to eq 'testforest.local'
      end

      it 'stores the strings with UTF-8 encoding' do
        client.store_target_info(target_info_str)
        expect(client.default_name.encoding.name).to eq 'UTF-8'
        expect(client.default_domain.encoding.name).to eq 'UTF-8'
        expect(client.dns_host_name.encoding.name).to eq 'UTF-8'
        expect(client.dns_domain_name.encoding.name).to eq 'UTF-8'
        expect(client.dns_tree_name.encoding.name).to eq 'UTF-8'
      end
    end

    describe '#extract_os_version' do
      it 'returns the expected version number' do
        expect(client.extract_os_version("\x06\x00q\x17\x00\x00\x00\x0F")).to eq '6.0.6001'
      end
    end
  end

  context 'Signing' do
    describe '#smb2_sign' do
      let(:request1) {
        packet = RubySMB::SMB2::Packet::SessionSetupRequest.new
        packet.smb2_header.flags.signed = 1
        packet.smb2_header.signature = "\x00" * 16
        packet
      }
      let(:fake_hmac) { "\x31\x07\x78\x3e\x35\xd7\x0e\x89\x08\x43\x8a\x18\xcd\x78\x52\x39".force_encoding('ASCII-8BIT') }

      context 'if signing is required and we have a session key' do
        it 'generates the HMAC based on the packet and the NTLM session key and signs the packet with it' do
          smb2_client.session_key = 'foo'
          smb2_client.signing_required = true
          expect(OpenSSL::HMAC).to receive(:digest).with(instance_of(OpenSSL::Digest), smb2_client.session_key, request1.to_binary_s).and_return(fake_hmac)
          expect(smb2_client.smb2_sign(request1).smb2_header.signature).to eq fake_hmac
        end
      end

      context 'when signing is not required' do
        it 'returns the packet exactly as it was given' do
          smb2_client.session_key = 'foo'
          smb2_client.signing_required = false
          expect(smb2_client.smb2_sign(request1)).to eq request1
        end
      end

      context 'when there is no session_key' do
        it 'returns the packet exactly as it was given' do
          smb2_client.session_key = ''
          smb2_client.signing_required = true
          expect(smb2_client.smb2_sign(request1)).to eq request1
        end
      end
    end

    describe '#smb1_sign' do
      let(:request1) { RubySMB::SMB1::Packet::SessionSetupRequest.new }
      let(:fake_sig) { "\x9f\x62\xcf\x08\xd9\xc2\x83\x21".force_encoding('ASCII-8BIT') }

      context 'if signing is required and we have a session key' do
        it 'generates the signature based on the packet, the sequence counter and the NTLM session key and signs the packet with it' do
          smb1_client.session_key = 'foo'
          smb1_client.signing_required = true
          raw = request1.to_binary_s
          adjusted_request = RubySMB::SMB1::Packet::SessionSetupRequest.read(raw)
          adjusted_request.smb_header.security_features = [smb1_client.sequence_counter].pack('Q<')
          expect(OpenSSL::Digest::MD5).to receive(:digest).and_return(fake_sig)
          expect(smb1_client.smb1_sign(request1).smb_header.security_features).to eq fake_sig
        end
      end

      context 'when signing is not required' do
        it 'returns the packet exactly as it was given' do
          smb1_client.session_key = 'foo'
          smb1_client.signing_required = false
          expect(smb1_client.smb1_sign(request1)).to eq request1
        end
      end

      context 'when there is no session_key' do
        it 'returns the packet exactly as it was given' do
          smb1_client.session_key = ''
          smb1_client.signing_required = true
          expect(smb1_client.smb1_sign(request1)).to eq request1
        end
      end
    end

    describe '#smb3_sign' do
      context 'if signing is required and we have a session key' do
        let(:request) {
          packet = RubySMB::SMB2::Packet::SessionSetupRequest.new
          packet.smb2_header.flags.signed = 1
          packet.smb2_header.signature = "\x00" * 16
          packet
        }
        let(:session_key) { 'Session Key' }
        before :example do
          smb3_client.session_key = session_key
          smb3_client.signing_required = true
        end

        ['0x0300', '0x0302'].each do |dialect|
          context "with #{dialect} dialect" do
            it 'generates the signing key based on the session key and specific strings, and sign the packet with CMAC' do
              smb3_client.dialect = dialect
              fake_hash = "\x34\xc0\x40\xfe\x87\xcf\x49\x3d\x37\x87\x52\xd0\xd5\xf5\xfb\x86".b
              signing_key = RubySMB::Crypto::KDF.counter_mode(session_key, "SMB2AESCMAC\x00", "SmbSign\x00")
              expect(RubySMB::Crypto::KDF).to receive(:counter_mode).with(session_key, "SMB2AESCMAC\x00", "SmbSign\x00").and_call_original
              expect(OpenSSL::CMAC).to receive(:digest).with('AES', signing_key, request.to_binary_s).and_call_original
              expect(smb3_client.smb3_sign(request).smb2_header.signature).to eq fake_hash
            end
          end
        end

        context "with 0x0311 dialect" do
          it 'generates the signing key based on the session key, the preauth integrity hash and specific strings, and sign the packet with CMAC' do
            smb3_client.dialect = '0x0311'
            preauth_integrity_hash_value = 'Preauth Integrity Hash'
            fake_hash = "\x0e\x49\x6f\x8e\x74\x7c\xf2\xa0\x88\x5e\x9d\x54\xff\x0d\x0d\xfa".b
            smb3_client.preauth_integrity_hash_value = preauth_integrity_hash_value
            signing_key = RubySMB::Crypto::KDF.counter_mode(session_key, "SMBSigningKey\x00", preauth_integrity_hash_value)
            expect(RubySMB::Crypto::KDF).to receive(:counter_mode).with(session_key, "SMBSigningKey\x00", preauth_integrity_hash_value).and_call_original
            expect(OpenSSL::CMAC).to receive(:digest).with('AES', signing_key, request.to_binary_s).and_call_original
            expect(smb3_client.smb3_sign(request).smb2_header.signature).to eq fake_hash
          end
        end

        context 'with an incompatible dialect' do
          it 'raises the expected exception' do
            smb3_client.dialect = '0x0202'
            expect { smb3_client.smb3_sign(request) }.to raise_error(
              RubySMB::Error::SigningError,
              'Dialect "0x0202" is incompatible with SMBv3 signing'
            )
          end
        end
      end

      context 'if signing is not required but it is a TreeConnectRequest and we have a session key' do
        let(:request) {
          packet = RubySMB::SMB2::Packet::TreeConnectRequest.new
          packet.smb2_header.flags.signed = 1
          packet.smb2_header.signature = "\x00" * 16
          packet
        }
        let(:session_key) { 'Session Key' }
        before :example do
          smb3_client.session_key = session_key
          smb3_client.signing_required = false
        end

        ['0x0300', '0x0302'].each do |dialect|
          context "with #{dialect} dialect" do
            it 'generates the signing key based on the session key and specific strings, and sign the packet with CMAC' do
              smb3_client.dialect = dialect
              fake_hash = "\x34\x9e\x28\xb9\x50\x08\x34\x31\xc0\x83\x9d\xba\x56\xa5\x70\xa4".b
              signing_key = RubySMB::Crypto::KDF.counter_mode(session_key, "SMB2AESCMAC\x00", "SmbSign\x00")
              expect(RubySMB::Crypto::KDF).to receive(:counter_mode).with(session_key, "SMB2AESCMAC\x00", "SmbSign\x00").and_call_original
              expect(OpenSSL::CMAC).to receive(:digest).with('AES', signing_key, request.to_binary_s).and_call_original
              expect(smb3_client.smb3_sign(request).smb2_header.signature).to eq fake_hash
            end
          end
        end

        context "with 0x0311 dialect" do
          it 'generates the signing key based on the session key, the preauth integrity hash and specific strings, and sign the packet with CMAC' do
            smb3_client.dialect = '0x0311'
            preauth_integrity_hash_value = 'Preauth Integrity Hash'
            fake_hash = "\x83\xd9\x31\x39\x60\x46\xbe\x1e\x29\x34\xc8\xcf\x8c\x8e\xb4\x73".b
            smb3_client.preauth_integrity_hash_value = preauth_integrity_hash_value
            signing_key = RubySMB::Crypto::KDF.counter_mode(session_key, "SMBSigningKey\x00", preauth_integrity_hash_value)
            expect(RubySMB::Crypto::KDF).to receive(:counter_mode).with(session_key, "SMBSigningKey\x00", preauth_integrity_hash_value).and_call_original
            expect(OpenSSL::CMAC).to receive(:digest).with('AES', signing_key, request.to_binary_s).and_call_original
            expect(smb3_client.smb3_sign(request).smb2_header.signature).to eq fake_hash
          end
        end

        context 'with an incompatible dialect' do
          it 'raises the expected exception' do
            smb3_client.dialect = '0x0202'
            expect { smb3_client.smb3_sign(request) }.to raise_error(
              RubySMB::Error::SigningError,
              'Dialect "0x0202" is incompatible with SMBv3 signing'
            )
          end
        end
      end
    end
  end

  context '#increment_smb_message_id' do
    let(:request_packet) { RubySMB::SMB2::Packet::NegotiateRequest.new }

    it 'sets the message_id on the packet header to the client message_id' do
      id = client.smb2_message_id
      expect(client.increment_smb_message_id(request_packet).smb2_header.message_id).to eq id
    end

    it 'increments the client message id' do
      client.smb2_message_id = 1
      expect { client.increment_smb_message_id(request_packet) }.to change { client.smb2_message_id }.by(1)
    end
  end

  context 'connecting to a share' do
    let(:path) { '\\192.168.1.1\example' }
    let(:tree_id) { 2049 }
    context 'with SMB1' do
      let(:request) { RubySMB::SMB1::Packet::TreeConnectRequest.new }
      let(:response) {
        packet = RubySMB::SMB1::Packet::TreeConnectResponse.new
        packet.smb_header.tid = tree_id
        packet.parameter_block.access_rights.read("\xff\x01\x1f\x00")
        packet.data_block.service = 'A:'
        packet
      }

      describe '#smb1_tree_connect' do
        it 'builds and sends a TreeconnectRequest for the supplied share' do
          allow(RubySMB::SMB1::Packet::TreeConnectRequest).to receive(:new).and_return(request)
          modified_request = request
          modified_request.data_block.path = path
          expect(smb1_client).to receive(:send_recv).with(modified_request).and_return(response.to_binary_s)
          smb1_client.smb1_tree_connect(path)
        end

        it 'sends the response to #smb1_tree_from_response' do
          expect(smb1_client).to receive(:send_recv).and_return(response.to_binary_s)
          expect(smb1_client).to receive(:smb1_tree_from_response).with(path, response)
          smb1_client.smb1_tree_connect(path)
        end
      end

      describe '#smb1_tree_from_response' do
        it 'raises an InvalidPacket exception if the command is not TREE_CONNECT' do
          response.smb_header.command = RubySMB::SMB1::Commands::SMB_COM_NEGOTIATE
          expect { smb1_client.smb1_tree_from_response(path, response) }.to raise_error(RubySMB::Error::InvalidPacket)
        end

        it 'raises an UnexpectedStatusCode exception if we do not get STATUS_SUCCESS' do
          response.smb_header.nt_status = 0xc0000015
          expect { smb1_client.smb1_tree_from_response(path, response) }.to raise_error(
            RubySMB::Error::UnexpectedStatusCode,
            'The server responded with an unexpected status code: STATUS_NONEXISTENT_SECTOR'
          )
        end

        it 'creates a new Tree from itself, the share path, and the response packet' do
          expect(RubySMB::SMB1::Tree).to receive(:new).with(client: smb1_client, share: path, response: response)
          smb1_client.smb1_tree_from_response(path, response)
        end
      end
    end

    context 'with SMB2' do
      let(:request) { RubySMB::SMB2::Packet::TreeConnectRequest.new }
      let(:response) {
        packet = RubySMB::SMB2::Packet::TreeConnectResponse.new
        packet.smb2_header.tree_id = tree_id
        packet.maximal_access.read("\xff\x01\x1f\x00")
        packet.share_type = 0x01
        packet
      }

      describe '#smb2_tree_connect' do
        it 'builds and sends the expected TreeconnectRequest for the supplied share' do
          allow(RubySMB::SMB2::Packet::TreeConnectRequest).to receive(:new).and_return(request)
          expect(smb2_client).to receive(:send_recv) do |req|
            expect(req).to eq(request)
            expect(req.smb2_header.tree_id).to eq(65_535)
            expect(req.path).to eq(path.encode('UTF-16LE'))
            response.to_binary_s
          end
          smb2_client.smb2_tree_connect(path)
        end

        it 'sends the response to #smb2_tree_from_response' do
          expect(smb2_client).to receive(:send_recv).and_return(response.to_binary_s)
          expect(smb2_client).to receive(:smb2_tree_from_response).with(path, response)
          smb2_client.smb2_tree_connect(path)
        end
      end

      describe '#smb2_tree_from_response' do
        it 'raises an InvalidPacket exception if the command is not TREE_CONNECT' do
          response.smb2_header.command = RubySMB::SMB2::Commands::NEGOTIATE
          expect { smb2_client.smb2_tree_from_response(path, response) }.to raise_error(RubySMB::Error::InvalidPacket)
        end

        it 'raises an UnexpectedStatusCode exception if we do not get STATUS_SUCCESS' do
          response.smb2_header.nt_status = 0xc0000015
          expect { smb2_client.smb2_tree_from_response(path, response) }.to raise_error(
            RubySMB::Error::UnexpectedStatusCode,
            'The server responded with an unexpected status code: STATUS_NONEXISTENT_SECTOR'
          )
        end

        it 'creates a new Tree from itself, the share path, and the response packet' do
          expect(RubySMB::SMB2::Tree).to receive(:new).with(client: smb2_client, share: path, response: response, encrypt: false)
          smb2_client.smb2_tree_from_response(path, response)
        end

        it 'creates a new with encryption set if the response requires it' do
          response.share_flags.encrypt = 1
          expect(RubySMB::SMB2::Tree).to receive(:new).with(client: smb2_client, share: path, response: response, encrypt: true)
          smb2_client.smb2_tree_from_response(path, response)
        end
      end

      describe '#net_share_enum_all' do
        let(:tree){ double("Tree") }
        let(:named_pipe){ double("Named Pipe") }

        before :example do
          allow(tree).to receive(:open_pipe).and_return(named_pipe)
          allow(named_pipe).to receive(:net_share_enum_all)
        end

        context 'with SMB1' do
          before :example do
            allow(smb1_client).to receive(:tree_connect).and_return(tree)
          end

          it 'it calls the #tree_connect method to connect to the "host" IPC$ share' do
            ipc_share = "\\\\#{sock.peeraddr}\\IPC$"
            expect(smb1_client).to receive(:tree_connect).with(ipc_share).and_return(tree)
            smb1_client.net_share_enum_all(sock.peeraddr)
          end

          it 'it calls the Tree #open_pipe method to open "srvsvc" named pipe' do
            expect(tree).to receive(:open_pipe).with(filename: "srvsvc", write: true, read: true).and_return(named_pipe)
            smb1_client.net_share_enum_all(sock.peeraddr)
          end

          it 'it calls the File #net_share_enum_all method with the correct host' do
            host = "1.2.3.4"
            expect(named_pipe).to receive(:net_share_enum_all).with(host)
            smb1_client.net_share_enum_all(host)
          end
        end

        context 'with SMB2' do
          before :example do
            allow(smb2_client).to receive(:tree_connect).and_return(tree)
          end

          it 'it calls the #tree_connect method to connect to the "host" IPC$ share' do
            ipc_share = "\\\\#{sock.peeraddr}\\IPC$"
            expect(smb2_client).to receive(:tree_connect).with(ipc_share).and_return(tree)
            smb2_client.net_share_enum_all(sock.peeraddr)
          end

          it 'it calls the Tree #open_pipe method to open "srvsvc" named pipe' do
            expect(tree).to receive(:open_pipe).with(filename: "srvsvc", write: true, read: true).and_return(named_pipe)
            smb2_client.net_share_enum_all(sock.peeraddr)
          end

          it 'it calls the File #net_share_enum_all method with the correct host' do
            host = "1.2.3.4"
            expect(named_pipe).to receive(:net_share_enum_all).with(host)
            smb2_client.net_share_enum_all(host)
          end
        end
      end
    end
  end

  context 'Echo command' do
    context 'with SMB1' do
      let(:echo_request) { RubySMB::SMB1::Packet::EchoRequest.new }
      let(:echo_response) {
        packet = RubySMB::SMB1::Packet::EchoResponse.new
        packet.smb_header.nt_status = 0x00000080
        packet
      }

      before(:each) do
        allow(RubySMB::SMB2::Packet::EchoRequest).to receive(:new).and_return(echo_request)
      end

      it 'sets the echo_count on the request packet' do
        modified_request = echo_request
        modified_request.parameter_block.echo_count = 5
        expect(smb1_client).to receive(:send_recv).with(modified_request).and_return(echo_response.to_binary_s)
        expect(dispatcher).to receive(:recv_packet).exactly(4).times.and_return(echo_response.to_binary_s)
        smb1_client.smb1_echo(count: 5)
      end

      it 'sets the data on the request packet' do
        modified_request = echo_request
        modified_request.data_block.data = 'DEADBEEF'
        expect(smb1_client).to receive(:send_recv).with(modified_request).and_return(echo_response.to_binary_s)
        smb1_client.smb1_echo(data: 'DEADBEEF')
      end

      it 'returns the NT status code' do
        expect(smb1_client).to receive(:send_recv).and_return(echo_response.to_binary_s)
        expect(smb1_client.echo).to eq WindowsError::NTStatus::STATUS_ABANDONED
      end

      it 'raise an InvalidPacket exception when the response is not valid' do
        echo_response.smb_header.command = RubySMB::SMB1::Commands::SMB_COM_SESSION_SETUP_ANDX
        allow(smb1_client).to receive(:send_recv).and_return(echo_response.to_binary_s)
        expect { smb1_client.echo }.to raise_error(RubySMB::Error::InvalidPacket)
      end
    end

    context 'with SMB2' do
      let(:echo_request) { RubySMB::SMB2::Packet::EchoRequest.new }
      let(:echo_response) { RubySMB::SMB2::Packet::EchoResponse.new }

      it '#smb2_echo sends an Echo Request and returns a response' do
        allow(RubySMB::SMB2::Packet::EchoRequest).to receive(:new).and_return(echo_request)
        expect(smb2_client).to receive(:send_recv).with(echo_request).and_return(echo_response.to_binary_s)
        expect(smb2_client.smb2_echo).to eq echo_response
      end

      it 'raise an InvalidPacket exception when the response is not valid' do
        echo_response.smb2_header.command = RubySMB::SMB2::Commands::SESSION_SETUP
        allow(smb2_client).to receive(:send_recv).and_return(echo_response.to_binary_s)
        expect { smb2_client.smb2_echo }.to raise_error(RubySMB::Error::InvalidPacket)
      end
    end
  end

  context 'Winreg' do
    describe '#connect_to_winreg' do
      let(:host)       { '1.2.3.4' }
      let(:share)      { "\\\\#{host}\\IPC$" }
      let(:ipc_tree)   { double('IPC$ tree') }
      let(:named_pipe) { double('Named pipe') }
      before :example do
        allow(ipc_tree).to receive_messages(
          :share     => share,
          :open_pipe => named_pipe
        )
        allow(client).to receive(:tree_connect).and_return(ipc_tree)
      end

      context 'when the client is already connected to the IPC$ share' do
        before :example do
          client.tree_connects << ipc_tree
          allow(ipc_tree).to receive(:share).and_return(share)
        end

        it 'does not connect to the already connected tree' do
          client.connect_to_winreg(host)
          expect(client).to_not have_received(:tree_connect)
        end
      end

      it 'calls #tree_connect' do
        client.connect_to_winreg(host)
        expect(client).to have_received(:tree_connect).with(share)
      end

      it 'open \'winreg\' pipe on the IPC$ Tree' do
        client.connect_to_winreg(host)
        expect(ipc_tree).to have_received(:open_pipe).with(filename: "winreg", write: true, read: true)
      end

      it 'returns the expected opened named pipe' do
        expect(client.connect_to_winreg(host)).to eq(named_pipe)
      end

      context 'when a block is given' do
        before :example do
          allow(named_pipe).to receive(:close)
        end

        it 'yields the expected named_pipe' do
          client.connect_to_winreg(host) do |np|
            expect(np).to eq(named_pipe)
          end
        end

        it 'closes the named pipe' do
          client.connect_to_winreg(host) { |np| }
          expect(named_pipe).to have_received(:close)
        end

        it 'returns the block return value' do
          result = double('Result')
          expect(client.connect_to_winreg(host) { |np| result }).to eq(result)
        end
      end
    end

    describe '#has_registry_key?' do
      let(:host)       { '1.2.3.4' }
      let(:key)        { 'HKLM\\Registry\\Key' }
      let(:named_pipe) { double('Named pipe') }
      let(:result)     { double('Result') }
      before :example do
        allow(client).to receive(:connect_to_winreg).and_yield(named_pipe)
        allow(named_pipe).to receive(:has_registry_key?).and_return(result)
      end

      it 'calls #connect_to_winreg to wrap the main logic around' do
        client.has_registry_key?(host, key)
        expect(client).to have_received(:connect_to_winreg).with(host)
      end

      it 'calls Pipe #has_registry_key?' do
        client.has_registry_key?(host, key)
        expect(named_pipe).to have_received(:has_registry_key?).with(key)
      end
    end

    describe '#read_registry_key_value' do
      let(:host)        { '1.2.3.4' }
      let(:key)         { 'HKLM\\Registry\\Key' }
      let(:value_name)  { 'Value' }
      let(:named_pipe)  { double('Named pipe') }
      let(:result)      { double('Result') }
      before :example do
        allow(client).to receive(:connect_to_winreg).and_yield(named_pipe)
        allow(named_pipe).to receive(:read_registry_key_value).and_return(result)
      end

      it 'calls #connect_to_winreg to wrap the main logic around' do
        client.read_registry_key_value(host, key, value_name)
        expect(client).to have_received(:connect_to_winreg).with(host)
      end

      it 'calls Pipe #read_registry_key_value' do
        client.read_registry_key_value(host, key, value_name)
        expect(named_pipe).to have_received(:read_registry_key_value).with(key, value_name)
      end
    end

    describe '#enum_registry_key' do
      let(:host)       { '1.2.3.4' }
      let(:key)        { 'HKLM\\Registry\\Key' }
      let(:named_pipe) { double('Named pipe') }
      let(:result)     { double('Result') }
      before :example do
        allow(client).to receive(:connect_to_winreg).and_yield(named_pipe)
        allow(named_pipe).to receive(:enum_registry_key).and_return(result)
      end

      it 'calls #connect_to_winreg to wrap the main logic around' do
        client.enum_registry_key(host, key)
        expect(client).to have_received(:connect_to_winreg).with(host)
      end

      it 'calls Pipe #enum_registry_key' do
        client.enum_registry_key(host, key)
        expect(named_pipe).to have_received(:enum_registry_key).with(key)
      end
    end

    describe '#enum_registry_values' do
      let(:host)       { '1.2.3.4' }
      let(:key)        { 'HKLM\\Registry\\Key' }
      let(:named_pipe) { double('Named pipe') }
      let(:result)     { double('Result') }
      before :example do
        allow(client).to receive(:connect_to_winreg).and_yield(named_pipe)
        allow(named_pipe).to receive(:enum_registry_values).and_return(result)
      end

      it 'calls #connect_to_winreg to wrap the main logic around' do
        client.enum_registry_values(host, key)
        expect(client).to have_received(:connect_to_winreg).with(host)
      end

      it 'calls Pipe #enum_registry_values' do
        client.enum_registry_values(host, key)
        expect(named_pipe).to have_received(:enum_registry_values).with(key)
      end
    end
  end

  describe '#update_preauth_hash' do
    it 'raises an EncryptionError exception if the preauth integrity hash algorithm is not known' do
      expect { client.update_preauth_hash('Test') }.to raise_error(
        RubySMB::Error::EncryptionError,
        'Cannot compute the Preauth Integrity Hash value: Preauth Integrity Hash Algorithm is nil'
      )
    end

    it 'computes the hash value' do
      packet = RubySMB::SMB2::Packet::EchoRequest.new
      data = 'Previous hash'
      algo = RubySMB::SMB2::PreauthIntegrityCapabilities::HASH_ALGORITM_MAP[
        RubySMB::SMB2::PreauthIntegrityCapabilities::SHA_512
      ]
      client.preauth_integrity_hash_algorithm = algo
      client.preauth_integrity_hash_value = data
      hash = OpenSSL::Digest.digest(algo, data + packet.to_binary_s)
      client.update_preauth_hash(packet)
      expect(client.preauth_integrity_hash_value).to eq(hash)
    end
  end

  context 'Encryption' do
    describe '#smb3_encrypt' do
      let(:transform_packet) { double('TransformHeader packet') }
      let(:session_key) { "\x5c\x00\x4a\x3b\xf0\xa2\x4f\x75\x4c\xb2\x74\x0a\xcf\xc4\x8e\x1a".b }
      let(:data) { RubySMB::SMB2::Packet::TreeConnectRequest.new.to_binary_s }

      before :example do
        allow(RubySMB::SMB2::Packet::TransformHeader).to receive(:new).and_return(transform_packet)
        allow(transform_packet).to receive(:encrypt)
        client.session_key = session_key
      end

      it 'does not generate a new client encryption key if it already exists' do
        client.client_encryption_key = 'key'
        expect(RubySMB::Crypto::KDF).to_not receive(:counter_mode)
        expect(client.client_encryption_key).to eq('key')
        client.smb3_encrypt(data)
      end

      ['0x0300', '0x0302'].each do |dialect|
        context "with #{dialect} dialect" do
          before :example do
            client.dialect = dialect
            client.encryption_algorithm = 'AES-128-CCM'
          end

          it 'generates the client encryption key with the expected parameters' do
            expect(RubySMB::Crypto::KDF).to receive(:counter_mode).with(
              session_key,
              "SMB2AESCCM\x00",
              "ServerIn \x00",
              length: 128
            ).and_call_original
            client.smb3_encrypt(data)
          end
        end
      end

      context 'with 0x0311 dialect' do
        it 'generates the client encryption key with the expected parameters' do
          client.preauth_integrity_hash_value = ''
          client.dialect = '0x0311'
          client.encryption_algorithm = 'AES-128-CCM'
          expect(RubySMB::Crypto::KDF).to receive(:counter_mode).with(
            session_key,
            "SMBC2SCipherKey\x00",
            '',
            length: 128
          ).and_call_original
          client.smb3_encrypt(data)
        end
      end

      it 'raises the expected exception if the dialect is incompatible' do
        client.dialect = '0x0202'
        expect { client.smb3_encrypt(data) }.to raise_error(RubySMB::Error::EncryptionError)
      end

      it 'creates a TransformHeader packet and encrypt the data' do
        client.dialect = '0x0300'
        client.encryption_algorithm = 'AES-128-CCM'
        client.session_id = 123
        client.smb3_encrypt(data)
        expect(RubySMB::SMB2::Packet::TransformHeader).to have_received(:new).with(flags: 1, session_id: 123)
        expect(transform_packet).to have_received(:encrypt).with(data, client.client_encryption_key, algorithm: 'AES-128-CCM')
      end

      it 'generates the expected client encryption key with 0x0302 dialect' do
        client.dialect = '0x0302'
        client.encryption_algorithm = 'AES-128-CCM'
        expected_enc_key =
          "\xa4\xfa\x23\xc1\xb0\x65\x84\xce\x47\x08\x5b\xe0\x64\x98\xd7\x87".b
        client.smb3_encrypt(data)
        expect(client.client_encryption_key).to eq expected_enc_key
      end

      it 'generates the expected client encryption key with 0x0311 dialect' do
        client.dialect = '0x0311'
        client.encryption_algorithm = 'AES-128-CCM'
        client.session_key =
          "\x5c\x00\x4a\x3b\xf0\xa2\x4f\x75\x4c\xb2\x74\x0a\xcf\xc4\x8e\x1a".b
        client.preauth_integrity_hash_value =
          "\x57\x77\x7d\x47\xc2\xa9\xc8\x23\x6e\x8a\xfa\x39\xe8\x77\x2f\xb0\xb6"\
          "\x01\xba\x85\x58\x77\xf5\x01\xa0\xf0\x31\x69\x6a\x64\x49\x1c\x61\xdb"\
          "\x57\x34\x19\x1b\x80\x33\x9a\xfa\x1d\x6c\x3f\xca\x44\x68\x78\x5b\xb9"\
          "\xda\x41\xfa\x83\xe5\xa9\x6f\xcf\x44\xbc\xe5\x26\x6e".b
        expected_enc_key =
          "\xc7\x4e\xfe\x4d\x15\x48\x5b\x0b\x71\x45\x49\x26\x8a\xd9\x6c\xaa".b
        client.smb3_encrypt(data)
        expect(client.client_encryption_key).to eq expected_enc_key
      end
    end

    describe '#smb3_decrypt' do
      let(:transform_packet) { double('TransformHeader packet') }
      let(:session_key) { "\x5c\x00\x4a\x3b\xf0\xa2\x4f\x75\x4c\xb2\x74\x0a\xcf\xc4\x8e\x1a".b }

      before :example do
        allow(transform_packet).to receive(:decrypt)
        client.session_key = session_key
      end

      it 'does not generate a new server encryption key if it already exists' do
        client.server_encryption_key = 'key'
        expect(RubySMB::Crypto::KDF).to_not receive(:counter_mode)
        expect(client.server_encryption_key).to eq('key')
        client.smb3_decrypt(transform_packet)
      end

      ['0x0300', '0x0302'].each do |dialect|
        context "with #{dialect} dialect" do
          before :example do
            client.dialect = dialect
            client.encryption_algorithm = 'AES-128-CCM'
          end

          it 'generates the client encryption key with the expected parameters' do
            expect(RubySMB::Crypto::KDF).to receive(:counter_mode).with(
              session_key,
              "SMB2AESCCM\x00",
              "ServerOut\x00",
              length: 128
            ).and_call_original
            client.smb3_decrypt(transform_packet)
          end
        end
      end

      context 'with 0x0311 dialect' do
        it 'generates the client encryption key with the expected parameters' do
          client.preauth_integrity_hash_value = ''
          client.dialect = '0x0311'
          client.encryption_algorithm = 'AES-128-CCM'
          expect(RubySMB::Crypto::KDF).to receive(:counter_mode).with(
            session_key,
            "SMBS2CCipherKey\x00",
            '',
            length: 128
          ).and_call_original
          client.smb3_decrypt(transform_packet)
        end
      end

      it 'raises the expected exception if the dialect is incompatible' do
        client.dialect = '0x0202'
        expect { client.smb3_decrypt(transform_packet) }.to raise_error(RubySMB::Error::EncryptionError)
      end

      it 'creates a TransformHeader packet and encrypt the data' do
        client.dialect = '0x0300'
        client.encryption_algorithm = 'AES-128-CCM'
        client.session_id = 123
        client.smb3_decrypt(transform_packet)
        expect(transform_packet).to have_received(:decrypt).with(client.server_encryption_key, algorithm: 'AES-128-CCM')
      end

      it 'generates the expected server encryption key with 0x0302 dialect' do
        client.dialect = '0x0302'
        client.encryption_algorithm = 'AES-128-CCM'
        expected_enc_key =
          "\x65\x21\xd3\x6d\xe9\xe3\x5a\x66\x09\x61\xae\x3e\xc6\x49\x6b\xdf".b
        client.smb3_decrypt(transform_packet)
        expect(client.server_encryption_key).to eq expected_enc_key
      end

      it 'generates the expected server encryption key with 0x0311 dialect' do
        client.dialect = '0x0311'
        client.encryption_algorithm = 'AES-128-CCM'
        client.session_key =
          "\x5c\x00\x4a\x3b\xf0\xa2\x4f\x75\x4c\xb2\x74\x0a\xcf\xc4\x8e\x1a".b
        client.preauth_integrity_hash_value =
          "\x57\x77\x7d\x47\xc2\xa9\xc8\x23\x6e\x8a\xfa\x39\xe8\x77\x2f\xb0\xb6"\
          "\x01\xba\x85\x58\x77\xf5\x01\xa0\xf0\x31\x69\x6a\x64\x49\x1c\x61\xdb"\
          "\x57\x34\x19\x1b\x80\x33\x9a\xfa\x1d\x6c\x3f\xca\x44\x68\x78\x5b\xb9"\
          "\xda\x41\xfa\x83\xe5\xa9\x6f\xcf\x44\xbc\xe5\x26\x6e".b
        expected_enc_key =
          "\x8c\x2c\x31\x15\x66\xba\xa9\xab\xcf\xb2\x47\x8d\x72\xd5\xd7\x4a".b
        client.smb3_decrypt(transform_packet)
        expect(client.server_encryption_key).to eq expected_enc_key
      end
    end
  end
end
