hgoodman/asa-console

View on GitHub
lib/asa_console/terminal/fake_ssh.rb

Summary

Maintainability
A
1 hr
Test Coverage

require 'asa_console/terminal/ssh'

class ASAConsole
  module Terminal
    #
    # A subclass of {SSH} that overrides the {SSH#connect} method to generate
    # stub objects for RSpec testing.
    #
    # The constructor takes the same hash options as {SSH#initialize}, but
    # requires one additional option; `:input_proc`, a proc that takes two
    # arguments:
    #   * `input` -- A command or other input that is being passed to the
    #     simulated appliance.
    #   * `prompt` -- Last prompt displayed on the terminal when the `input`
    #     text was entered.
    #
    # The proc is expected to return an array with three elements:
    #   * `output` -- Result of sending `input` to the simulated appliance.
    #   * `prompt` -- Next prompt to present within the session.
    #   * `disconnect` -- `true` to indicate that the SSH connection has been
    #     closed (e.g. if "exit" was sent to the terminal), or `false`
    #     otherwise.
    #
    # @example
    #   input_proc = proc do |input|
    #     prompt = 'TEST# '
    #     case input
    #     when "terminal pager lines 0\n", nil
    #       output = prompt
    #       disconnect = false
    #     when "exit\n"
    #       output = "\nLogoff\n\n"
    #       disconnect = true
    #     else
    #       output = "ERROR: Command not implemented\n" + prompt
    #       disconnect = false
    #     end
    #     [output, prompt, disconnect]
    #   end
    #
    #   asa = ASAConsole.fake_ssh(
    #     host: 'ignored',
    #     user: 'ignored',
    #     input_proc: input_proc
    #   )
    #   asa.connect
    #
    #   # This will raise an error
    #   asa.priv_exec('write memory')
    #
    # @api development
    class FakeSSH < SSH
      #
      # @api private
      attr_accessor :raw_buffer
      #
      # @api private
      attr_accessor :raw_session_log
      #
      # @api private
      attr_accessor :session
      #
      # @api private
      attr_accessor :channel
      #
      # @api private
      attr_accessor :last_output_received
      #
      # @api private
      attr_accessor :input_proc

      # Wrapper for the {SSH} class constructor that extracts `:input_proc` from
      # the options hash and passes the rest to the parent.
      #
      # @option opts [Proc] :input_proc
      # @option opts ... options for the {SSH} constructor
      # @see SSH#initialize SSH constructor
      def initialize(opts)
        @input_proc = opts.delete(:input_proc)
        fail Error::MissingOptionError, 'Option :input_proc is missing or invalid' \
          unless @input_proc.is_a? Proc
        super(opts)
      end

      # Connect to a simulated appliance. This method creates anonymous stub
      # classes that take the place of `Net::SSH::Connection::Session` and
      # `Net::SSH::Connection::Channel`. The stub classes implement only the
      # subset of methods that are used by the {SSH} terminal class.
      #
      # @return [void]
      def connect
        @session = Class.new do
          def initialize
            @closed = false
          end
          def close
            @closed = true
          end
          def closed?
            @closed ? true : false
          end
        end.new

        @channel = Class.new do
          attr_reader :terminal
          def initialize(terminal)
            @terminal = terminal
            @input_buffer = ''
            @output_buffer, @prompt = @terminal.input_proc.call
          end
          def connection
            self
          end
          def process(timeout)
            sleep timeout
            @terminal.raw_buffer = @output_buffer
            @terminal.raw_session_log << @output_buffer
            @output_buffer = ''
            fail Net::SSH::Disconnect if @terminal.session.closed?
          end
          def send_data(input)
            @input_buffer << input
            return unless @input_buffer.end_with? "\n"
            @output_buffer << @input_buffer
            output, prompt, disconnect = @terminal.input_proc.call(@input_buffer, @prompt)
            @output_buffer << output
            @prompt = prompt
            @terminal.session.close if disconnect
            @input_buffer = ''
            @terminal.last_output_received = Time.now.getlocal
          end
        end.new(self)

        @connected = true

        # This is copied verbatim from the parent #connect method
        expect(ANY_EXEC_PROMPT) do |success, output|
          @on_output_callbacks.each { |c| c.call(nil, nil, output) }
          fail Error::ConnectFailure 'Failed to parse EXEC prompt', self unless success
        end
      end
    end
  end
end