ruby-on-railsrspecnet-ssh

Rails RSpec net-ssh mock return of second ssh request


I'm working on a web application that frequently access simulation data on a remote server. I want to create test for errors handling that might happen during these request.

The problem I currently have is I cannot seems to mock a request with my ssh_with_stderr method. The ssh method works fine.

This the code I'm trying to test:

# app/jobs/zip_files_sync_job.rb

class ZipFilesSyncJob < ApplicationJob
  queue_as :default
  discard_on ActiveJob::DeserializationError

  def perform(simulation)
    simulation.zip_files.each do |f|
      if f.path.nil? && f.created_at < 6.hours.ago
        f.state = 'error'
        f.save!
        next
      end
      next if f.path.nil?

      _, errors = simulation.server.ssh_with_stderr("ls #{f.path.shellescape}")
      if errors.blank?
        f.size = f.simulation.server.ssh("stat -c %s #{f.path.shellescape}")
        f.state = 'ready' if f.size.to_i.positive?
      elsif f.state == 'ready' && errors.present?
        f.state = 'error'
      elsif f.state == 'zipping' && errors.present? && f.created_at < 6.hours.ago
        f.state = 'error'
      end
      f.save!
    end
  end
end

And this is what I want to test:

# spec/jobs/zip_files_sync_job_spec.rb

require 'rails_helper'

RSpec.describe ZipFilesSyncJob, type: :job do
  let(:private_group) { Group::PRIVATE }
  let(:user) { FactoryBot.create :user }
  let(:server) { FactoryBot.create :server, user: user, external_storage: false }
  let(:simulation) { FactoryBot.create :simulation, user: user, group: private_group, server: server }
  let(:zip_file) { FactoryBot.create :zip_file, simulation: simulation, path: 'test/zip_file', state: 'pending', size: '100' }
  let(:zip_file_no_path) { FactoryBot.create :zip_file, simulation: simulation, path: nil, created_at: 10.hours.ago, state: 'pending' }
  let(:ssh_connection) { double('net-ssh') }

  before do
    zip_file_no_path
    allow(Net::SSH).to receive(:start).and_yield(ssh_connection)
  end

  def perform_zip_file_sync(zip_file)
    perform_enqueued_jobs do
      ZipFilesSyncJob.perform_now(simulation)
    end
    zip_file.reload

    yield

    allow(Net::SSH).to receive(:start).and_call_original
  end

  describe '#perform' do
    include ActiveJob::TestHelper

   #################################
   ##### This test works fine #####
   #################################

    context 'with no errors' do
      before do
        zip_file
      end
      it 'it will change the state to ready' do
        allow(Net::SSH).to receive(:start).and_return('144371201')
        perform_zip_file_sync(zip_file) do
          expect(zip_file.state).to eq 'ready'
        end
      end
    end

   #############################################################################
   ##### This test fails because it does not return on the ssh_with_stderr #####
   #############################################################################

    context 'with errors' do
      it 'will change the state to error' do
        allow(Net::SSH).to receive(:start).and_return("[' ', 'Error with connection']")
        perform_enqueued_jobs do
          ZipFilesSyncJob.perform_now(simulation)
        end
        zip_file.reload
        expect(zip_file.state).to eq 'error'
      end
    end
  end
end

This the the code for the server connection. It uses the net-ssh gem

# app/models/server.rb

Class Server < ApplicationRecord

  def ssh(command, storage = true, &block)
    Net::SSH.start(hostname, username, port: port, keys: ["key"], non_interactive: true, timeout: 1) do |ssh|
      ssh.exec! "cd #{folder.shellescape}; #{command}", &block
    end
  end

  def ssh_with_stderr(command)
    @output = ""
    @errors = ""
    begin
      Net::SSH.start(hostname, username, port: port, keys: ["key"], non_interactive: true, timeout: 1) do |ssh|
        ssh.exec! "cd #{folder.shellescape}; #{command}" do |_ch, stream, data|
          if stream == :stderr
            @errors += data
          else
            @output += data
          end
        end
      end
    rescue Net::SSH::Exception, Errno::ECONNREFUSED, Errno::EINVAL, Errno::EADDRNOTAVAIL => e
      @output = nil
      @errors = e.message
     end
    [@output, @errors]
  end

Solution

  • With this mock

    allow(Net::SSH).to receive(:start).and_return("[' ', 'Error with connection']")
    

    the ssh_with_stderr looks like

    def ssh_with_stderr(command)
      @output = ""
      @errors = ""
      begin
        [' ', 'Error with connection']
      rescue Net::SSH::Exception, Errno::ECONNREFUSED, Errno::EINVAL, Errno::EADDRNOTAVAIL => e
        @output = nil
        @errors = e.message
       end
      [@output, @errors]
    end 
    

    So it always returns ["",""] , and checking errors.blank? always positive.

    Try to mock Net::SSH with and_raise instead of and_return, something like

    allow(Net::SSH).to receive(:start).and_raise(Errno::ECONNREFUSED, "Error with connection")