diff options
48 files changed, 1006 insertions, 91 deletions
| diff --git a/app/concerns/configurable.rb b/app/concerns/configurable.rb new file mode 100644 index 000000000..c7d0f1fd9 --- /dev/null +++ b/app/concerns/configurable.rb @@ -0,0 +1,26 @@ +module Configurable +   +  module ClassMethods +    def config &blk +      blk ? blk.(configuration) : configuration +    end + +    private +    def configuration +      @__configuration__ ||= Rails::Application::Configuration.new +    end +  end + +  module InstanceMethods +    private + +    def config +      self.class.config +    end +  end + +  def self.included(into) +    into.extend ClassMethods +    into.send :include, InstanceMethods +  end +end diff --git a/app/controllers/api/v1/netex_imports_controller.rb b/app/controllers/api/v1/netex_imports_controller.rb index 16a7cef69..d67d121c0 100644 --- a/app/controllers/api/v1/netex_imports_controller.rb +++ b/app/controllers/api/v1/netex_imports_controller.rb @@ -19,7 +19,7 @@ module Api        def netex_import_params          params            .require('netex_import') -          .permit(:file, :name, :referential_id) +          .permit(:file, :name, :referential_id, :workbench_id)        end      end    end diff --git a/app/models/api/v1/api_key.rb b/app/models/api/v1/api_key.rb index 7390db232..e1a7ab5a4 100644 --- a/app/models/api/v1/api_key.rb +++ b/app/models/api/v1/api_key.rb @@ -3,9 +3,20 @@ module Api      class ApiKey < ::ActiveRecord::Base        before_create :generate_access_token        belongs_to :referential, :class_name => '::Referential' +      validates_presence_of :referential -      def self.model_name -        ActiveModel::Name.new self, Api::V1, self.name.demodulize +      class << self +        def from(referential, name:) +          find_or_create_by!(name: name, referential: referential) +        end +        def model_name +          ActiveModel::Name.new  Api::V1, self.name.demodulize +        end +        def referential_from_token(token) +          array = token.split('-') +          return nil unless array.size==2 +          ::Referential.find( array.first) +        end        end        def eql?(other) @@ -13,16 +24,11 @@ module Api          other.token == self.token        end -      def self.referential_from_token(token) -        array = token.split('-') -        return nil unless array.size==2 -        ::Referential.find( array.first) -      end      private        def generate_access_token          begin -          self.token = "#{referential.id}-#{SecureRandom.hex}" +          self.token = "#{referential_id}-#{SecureRandom.hex}"          end while self.class.exists?(:token => self.token)        end      end diff --git a/app/models/import.rb b/app/models/import.rb index 535c676b1..c932ecdd9 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -9,6 +9,7 @@ class Import < ActiveRecord::Base    enumerize :status, in: %i(new pending successful failed running aborted canceled)    validates :file, presence: true +  validates_presence_of :referential, :workbench    before_create do      self.token_download = SecureRandom.urlsafe_base64 diff --git a/app/models/netex_import.rb b/app/models/netex_import.rb index 0cf4d0a7c..575cef816 100644 --- a/app/models/netex_import.rb +++ b/app/models/netex_import.rb @@ -2,6 +2,7 @@ require 'net/http'  class NetexImport < Import    after_commit :launch_java_import +    def launch_java_import      logger.warn  "Call iev get #{Rails.configuration.iev_url}/boiv_iev/referentials/importer/new?id=#{id}"      begin diff --git a/app/models/organisation.rb b/app/models/organisation.rb index d0742bda6..f697122aa 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*-  class Organisation < ActiveRecord::Base    include DataFormatEnumerations @@ -26,19 +25,12 @@ class Organisation < ActiveRecord::Base    def self.portail_api_request      conf = Rails.application.config.try(:stif_portail_api) -    raise 'Rails.application.config.stif_portail_api settings is not defined' unless conf +    raise 'Rails.application.config.stif_portail_api configuration is not defined' unless conf -    conn = Faraday.new(:url => conf[:url]) do |c| -      c.headers['Authorization'] = "Token token=\"#{conf[:key]}\"" -      c.adapter  Faraday.default_adapter -    end - -    resp = conn.get '/api/v1/organizations' -    if resp.status == 200 -      JSON.parse resp.body -    else -      raise "Error on api request status : #{resp.status} => #{resp.body}" -    end +    HTTPService.get_json_resource( +      host: conf[:url], +      path: '/api/v1/organizations', +      token: conf[:key])    end    def self.sync_update code, name, scope diff --git a/app/models/user.rb b/app/models/user.rb index c2aa14bda..37d35209a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -41,19 +41,12 @@ class User < ActiveRecord::Base    def self.portail_api_request      conf = Rails.application.config.try(:stif_portail_api) -    raise 'Rails.application.config.stif_portail_api settings is not defined' unless conf +    raise 'Rails.application.config.stif_portail_api configuration is not defined' unless conf -    conn = Faraday.new(:url => conf[:url]) do |c| -      c.headers['Authorization'] = %{Token token="#{conf[:key]}"} -      c.adapter  Faraday.default_adapter -    end - -    resp = conn.get '/api/v1/users' -    if resp.status == 200 -      JSON.parse resp.body -    else -      raise "Error on api request status : #{resp.status} => #{resp.body}" -    end +    HTTPService.get_json_resource( +      host: conf[:url], +      path: '/api/v1/users', +      token: conf[:key])    end    def self.portail_sync diff --git a/app/services/file_service.rb b/app/services/file_service.rb new file mode 100644 index 000000000..3b3ff3561 --- /dev/null +++ b/app/services/file_service.rb @@ -0,0 +1,24 @@ +# TODO: Delete me after stable implementation of #1726 +# module FileService extend self + +#   def unique_filename( path, enum_with: with_ints ) +#     file_names = enum_with.map( &file_name_maker(path) ) +#     file_names +#       .drop_while( &File.method(:exists?) ) +#       .next +#   end + +#   def with_ints(format='%d') +#     (0..Float::INFINITY) +#       .lazy +#       .map{ |n| format % n } +#   end +   + +#   private + +#   def file_name_maker path +#     ->(n){ [path, n].join('_') } +#   end + +# end diff --git a/app/services/http_service.rb b/app/services/http_service.rb new file mode 100644 index 000000000..ae7d0e413 --- /dev/null +++ b/app/services/http_service.rb @@ -0,0 +1,45 @@ +module HTTPService extend self + +  Timeout = Faraday::TimeoutError + +  def get_resource(host:, path:, token: nil, params: {}) +    Faraday.new(url: host) do |c| +      c.headers['Authorization'] = "Token token=#{token.inspect}" if token +      c.adapter Faraday.default_adapter + +      return c.get path, params +    end +  end + +  def get_json_resource(host:, path:, token: nil, params: {}) +    # Stupid Ruby!!! (I mean I just **need** Pattern Matching, maybe I need to write it myself :O) +    resp = get_resource(host: host, path: path, token: token, params: params)  +    if resp.status == 200 +      return JSON.parse(resp.body) +    else +      raise "Error on api request status : #{resp.status} => #{resp.body}" +    end +  end + +  # host: 'http://localhost:3000', +  # path: '/api/v1/netex_imports.json', +  # token: '13-74009c36638f587c9eafb1ce46e95585', +  # params: { netex_import: {referential_id: 13, workbench_id: 1}}, +  # upload: {file: [StringIO.new('howdy'), 'application/zip', 'greeting']}) +  def post_resource(host:, path:, token: nil, params: {}, upload: nil) +    Faraday.new(url: host) do |c| +      c.headers['Authorization'] = "Token token=#{token.inspect}" if token +      c.request :multipart +      c.request :url_encoded +      c.adapter Faraday.default_adapter + +      if upload +        name = upload.keys.first +        value, mime_type, as_name = upload.values.first +        params.update( name => Faraday::UploadIO.new(value, mime_type, as_name ) ) +      end + +      return c.post path, params +    end +  end +end diff --git a/app/services/retry_service.rb b/app/services/retry_service.rb new file mode 100644 index 000000000..21b1def36 --- /dev/null +++ b/app/services/retry_service.rb @@ -0,0 +1,54 @@ +require 'result' + +class RetryService + +  Retry = Class.new(RuntimeError) + +  # @param@ delays: +  # An array of delays that are used to retry after a sleep of the indicated +  # value in case of failed exceutions. +  # Once this array is exhausted the executen fails permanently +  # +  # @param@ rescue_from: +  # During execution all the excpetions from this array +plus RetryService::Retry+ are rescued from and +  # trigger just another retry after a `sleep` as indicated above. +  # +  # @param@ block: +  # This optional code is excuted before each retry, it is passed the result of the failed attempt, thus +  # an `Exception` and the number of execution already tried. +  def initialize( delays: [], rescue_from: [], &blk ) +    @intervals             = delays +    @registered_exceptions = Array(rescue_from) << Retry +    @failure_callback      = blk +  end + +  # @param@ blk: +  # The code to be executed it will be retried goverened by the `delay` passed into the initializer +  # as described there in case it fails with one of the predefined exceptions or `RetryService::Retry` +  # +  # Eventually it will return a `Result` object. +  def execute &blk +    result = execute_protected blk +    return result if result.ok? +    @intervals.each_with_index do | interval, retry_count | +      sleep interval +      @failure_callback.try(:call, result.value, retry_count + 1) +      result = execute_protected blk +      return result if result.ok? +    end +    result +  end + + +  private + +  def execute_protected blk +    Result.ok(blk.()) +  rescue Exception => e +    if @registered_exceptions.any?{ |re| e.is_a? re } +      Result.error(e) +    else +      raise +    end +  end +end diff --git a/app/services/zip_service.rb b/app/services/zip_service.rb new file mode 100644 index 000000000..778bfd06d --- /dev/null +++ b/app/services/zip_service.rb @@ -0,0 +1,55 @@ +class ZipService + +  attr_reader :current_entry, :zip_data + +  def initialize data +    @zip_data = data +    @current_entry = nil +  end + +  class << self +    def convert_entries entries +      -> output_stream do +        entries.each do |e| +          output_stream.put_next_entry e.name +          output_stream.write e.get_input_stream.read +        end +      end +    end + +    def entries input_stream +      Enumerator.new do |enum| +        loop{ enum << input_stream.get_next_entry } +      end.lazy.take_while{ |e| e } +    end +  end + +  def entry_groups +    self.class.entries(input_stream).group_by(&method(:entry_key)) +  end + +  def entry_group_streams +    entry_groups.map(&method(:make_stream)).to_h +  end + +  def entry_key entry +    entry.name.split('/', -1)[-2] +  end + +  def make_stream pair +    name, entries = pair  +    [name,  make_stream_from( entries )] +  end + +  def make_stream_from entries +    Zip::OutputStream.write_buffer(&self.class.convert_entries(entries)) +  end + +  def next_entry +    @current_entry = input_stream.get_next_entry +  end + +  def input_stream +    @__input_stream__ ||= Zip::InputStream.open(StringIO.new(zip_data)) +  end +end diff --git a/app/workers/workbench_import_worker.rb b/app/workers/workbench_import_worker.rb new file mode 100644 index 000000000..57c0f5f4e --- /dev/null +++ b/app/workers/workbench_import_worker.rb @@ -0,0 +1,118 @@ +class WorkbenchImportWorker +  include Sidekiq::Worker +  include Rails.application.routes.url_helpers +  include Configurable + +  RETRY_DELAYS = [3, 5, 8] + +  # Workers +  # ======= + +  def perform(import_id) +    @import     = WorkbenchImport.find(import_id) +    @response   = nil +    @import.update_attributes(status: 'running') +    downloaded  = download +    zip_service = ZipService.new(downloaded) +    upload zip_service +  end + +  def download +    logger.info  "HTTP GET #{import_url}" +    @zipfile_data = HTTPService.get_resource( +      host: import_host, +      path: import_path, +      params: {token: @import.token_download}).body +  end + +  def execute_post eg_name, eg_stream +    logger.info  "HTTP POST #{export_url} (for #{complete_entry_group_name(eg_name)})" +    HTTPService.post_resource( +      host: export_host, +      path: export_path, +      token: token(eg_name), +      params: params, +      upload: {file: [eg_stream, 'application/zip', eg_name]}) +  end + +  def log_failure reason, count +    logger.warn "HTTP POST failed with #{reason}, count = #{count}, response=#{@response}" +  end + +  def try_again +    raise RetryService::Retry +  end + +  def try_upload_entry_group eg_name, eg_stream +    result = execute_post eg_name, eg_stream +    return result if result && result.status < 400 +    @response = result.body +    try_again +  end + +  def upload zip_service +    entry_group_streams = zip_service.entry_group_streams +    @import.update_attributes total_steps: entry_group_streams.size +    entry_group_streams.each_with_index(&method(:upload_entry_group)) +  rescue StopIteration +    @import.update_attributes( current_step: entry_group_streams.size, status: 'failed' ) +  end + +  def upload_entry_group key_pair, element_count +    @import.update_attributes( current_step: element_count.succ ) +    retry_service = RetryService.new( +      delays: RETRY_DELAYS, +      rescue_from: [HTTPService::Timeout], +      &method(:log_failure))  +    status = retry_service.execute(&upload_entry_group_proc(key_pair)) +    raise StopIteration unless status.ok? +  end + +  def upload_entry_group_proc key_pair +    eg_name, eg_stream = key_pair +    # This should be fn.try_upload_entry_group(eg_name, eg_stream) ;( +    -> do +      try_upload_entry_group(eg_name, eg_stream) +    end +  end + + + +  # Queries +  # ======= + +  def complete_entry_group_name entry_group_name +    [@import.name, entry_group_name].join("--") +  end + +  def token entry_group_name +    Api::V1::ApiKey.from(@import.referential, name: complete_entry_group_name(entry_group_name)).token +  end + +  # Constants +  # ========= + +  def export_host +    Rails.application.config.rails_host +  end +  def export_path +    api_v1_netex_imports_path(format: :json) +  end +  def export_url +    @__export_url__ ||= File.join(export_host, export_path) +  end + +  def import_host +    Rails.application.config.rails_host +  end +  def import_path +    @__import_path__ ||= download_workbench_import_path(@import.workbench, @import) +  end +  def import_url +    @__import_url__ ||= File.join(import_host, import_path) +  end + +  def params +    @__params__ ||= { netex_import: { referential_id: @import.referential_id, workbench_id: @import.workbench_id } } +  end +end diff --git a/config/application.rb b/config/application.rb index 910ddd983..05a9752b6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -14,7 +14,7 @@ module ChouetteIhm      # Settings in config/environments/* take precedence over those specified here.      # Application configuration should go into files in config/initializers      # -- all .rb files in that directory are automatically loaded. -    config.autoload_paths << config.root.join("lib") +    config.autoload_paths << config.root.join('lib')      # custom exception pages      config.exceptions_app = self.routes diff --git a/config/environments/development.rb b/config/environments/development.rb index 5b2bd7402..ab4a29a3e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -82,7 +82,8 @@ Rails.application.configure do    config.portal_url = "http://stif-boiv-staging.af83.priv"    # IEV url -  config.iev_url = "http://localhost:8080" +  config.iev_url    = ENV.fetch('IEV_URL', 'http://localhost:8080') +  config.rails_host = ENV.fetch('FRONT_END_URL', 'http://localhost:3000')    # file to data for demo    config.demo_data = "tmp/demo.zip" diff --git a/config/environments/test.rb b/config/environments/test.rb index a6db12006..b3312be4a 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -62,6 +62,7 @@ Rails.application.configure do    # Reflex api url    config.reflex_api_url = "https://195.46.215.128/ws/reflex/V1/service=getData" +  config.rails_host = "http://www.example.com"    # file to data for demo    config.demo_data = "tmp/demo.zip" diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb index 29ce6564f..e1e86449c 100644 --- a/config/initializers/apartment.rb +++ b/config/initializers/apartment.rb @@ -18,34 +18,35 @@ Apartment.configure do |config|    # config.excluded_models = %w{Tenant}    #    config.excluded_models = [ -    "Referential", -    "ReferentialMetadata", -    "Organisation", -    "User", -    "Api::V1::ApiKey", -    "RuleParameterSet", -    "StopAreaReferential", -    "StopAreaReferentialMembership", -    "StopAreaReferentialSync", -    "StopAreaReferentialSyncMessage", -    "Chouette::StopArea", -    "LineReferential", -    "LineReferentialMembership", -    "LineReferentialSync", -    "LineReferentialSyncMessage", -    "Chouette::Line", -    "Chouette::GroupOfLine", -    "Chouette::Company", -    "Chouette::Network", -    "ReferentialCloning", -    "Workbench", -    "CleanUp", -    "CleanUpResult", -    "Calendar", -    "Import", -    "NetexImport", -    "ImportMessage", -    "ImportResource" +    'Referential', +    'ReferentialMetadata', +    'Organisation', +    'User', +    'Api::V1::ApiKey', +    'RuleParameterSet', +    'StopAreaReferential', +    'StopAreaReferentialMembership', +    'StopAreaReferentialSync', +    'StopAreaReferentialSyncMessage', +    'Chouette::StopArea', +    'LineReferential', +    'LineReferentialMembership', +    'LineReferentialSync', +    'LineReferentialSyncMessage', +    'Chouette::Line', +    'Chouette::GroupOfLine', +    'Chouette::Company', +    'Chouette::Network', +    'ReferentialCloning', +    'Workbench', +    'CleanUp', +    'CleanUpResult', +    'Calendar', +    'Import', +    'NetexImport', +    'WorkbenchImport', +    'ImportMessage', +    'ImportResource'    ]    # use postgres schemas? diff --git a/config/initializers/workbench_import.rb b/config/initializers/workbench_import.rb new file mode 100644 index 000000000..89ddd72ef --- /dev/null +++ b/config/initializers/workbench_import.rb @@ -0,0 +1,5 @@ +WorkbenchImportWorker.config do | config | +  config.dir = ENV.fetch('WORKBENCH_IMPORT_DIR'){ Rails.root.join 'tmp/workbench_import' } + +  FileUtils.mkdir_p config.dir +end diff --git a/db/migrate/20170727130705_add_current_step_and_total_steps_to_import.rb b/db/migrate/20170727130705_add_current_step_and_total_steps_to_import.rb new file mode 100644 index 000000000..b31e86f17 --- /dev/null +++ b/db/migrate/20170727130705_add_current_step_and_total_steps_to_import.rb @@ -0,0 +1,6 @@ +class AddCurrentStepAndTotalStepsToImport < ActiveRecord::Migration +  def change +    add_column :imports, :current_step, :integer, default: 0 +    add_column :imports, :total_steps, :integer, default: 0 +  end +end diff --git a/db/schema.rb b/db/schema.rb index b56027f48..05a024e1d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@  #  # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170715041954) do +ActiveRecord::Schema.define(version: 20170727130705) do    # These are extensions that must be enabled in order to support this database    enable_extension "plpgsql" @@ -287,6 +287,8 @@ ActiveRecord::Schema.define(version: 20170715041954) do      t.string   "type",                  limit: 255      t.integer  "parent_id",             limit: 8      t.string   "parent_type" +    t.integer  "current_step",                      default: 0 +    t.integer  "total_steps",                       default: 0    end    add_index "imports", ["referential_id"], name: "index_imports_on_referential_id", using: :btree diff --git a/lib/result.rb b/lib/result.rb new file mode 100644 index 000000000..96e03d323 --- /dev/null +++ b/lib/result.rb @@ -0,0 +1,37 @@ +# A value wrapper adding status information to any value +# Status can be :ok or :error, we are thusly implementing +# what is expressed in Elixir/Erlang as result tuples and +# in Haskell as `Data.Either` +class Result + +  attr_reader :status, :value +   +  class << self +    def ok value +      make :ok, value +    end +    def error value +      make :error, value +    end + +    def new *args +      raise NoMethodError, "No default constructor for #{self}" +    end + +    private +    def make status, value +      allocate.tap do | o | +        o.instance_exec do +          @status = status +          @value  = value +        end +      end +    end +  end + +  def ok?; status == :ok end + +  def == other +    other.kind_of?(self.class) && other.status == status && other.value == value +  end +end diff --git a/spec/concerns/configurable_spec.rb b/spec/concerns/configurable_spec.rb new file mode 100644 index 000000000..330241b72 --- /dev/null +++ b/spec/concerns/configurable_spec.rb @@ -0,0 +1,35 @@ +RSpec.describe Configurable do + +  subject do +    Class.new do +      include Configurable +    end +  end + +  let( :something ){ double('something') } + +  it 'can be configured' do +    expect{ subject.config.anything }.to raise_error(NoMethodError) + +    subject.config.something = something + +    expect( subject.config.something ).to eq(something) +    # Instances delegate to the class +    expect( subject.new.send(:config).something ).to eq(something) +    # **All** instances delegate to the class +    expect( subject.new.send(:config).something ).to eq(something) +  end + +  it 'can be configured with a block' do + +    subject.config do | c | +      c.something = something  +    end + +    expect( subject.config.something ).to eq(something) +    # Instances delegate to the class +    expect( subject.new.send(:config).something ).to eq(something) +    # **All** instances delegate to the class +    expect( subject.new.send(:config).something ).to eq(something) +  end +end diff --git a/spec/controllers/imports_controller_spec.rb b/spec/controllers/imports_controller_spec.rb index 7b575ab61..f07190496 100644 --- a/spec/controllers/imports_controller_spec.rb +++ b/spec/controllers/imports_controller_spec.rb @@ -15,6 +15,7 @@ RSpec.describe ImportsController, :type => :controller do      it 'should be successful' do        get :download, workbench_id: workbench.id, id: import.id, token: import.token_download        expect(response).to be_success +      expect( response.body ).to eq(import.file.read)      end    end  end diff --git a/spec/factories/api_keys.rb b/spec/factories/api_keys.rb new file mode 100644 index 000000000..bd31edecc --- /dev/null +++ b/spec/factories/api_keys.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do +  factory :api_key, class: Api::V1::ApiKey do +    token { "#{referential.id}-#{SecureRandom.hex}" } +    referential +  end +end diff --git a/spec/factories/import_tasks.rb b/spec/factories/import_tasks.rb deleted file mode 100644 index 9ca6db899..000000000 --- a/spec/factories/import_tasks.rb +++ /dev/null @@ -1,10 +0,0 @@ -FactoryGirl.define do -  factory :import_task do |f| -    user_name "dummy" -    user_id 123 -    no_save false -    format "Neptune" -    resources { Rack::Test::UploadedFile.new 'spec/fixtures/neptune.zip', 'application/zip', false } -    referential { Referential.find_by_slug("first") } -  end -end diff --git a/spec/factories/imports.rb b/spec/factories/imports.rb index e19fe92bb..aa9288fe9 100644 --- a/spec/factories/imports.rb +++ b/spec/factories/imports.rb @@ -13,5 +13,8 @@ FactoryGirl.define do      factory :netex_import, class: NetexImport do        file {File.open(Rails.root.join('spec', 'fixtures', 'terminated_job.json'))}      end +    factory :workbench_import, class: WorkbenchImport do +      file {File.open(Rails.root.join('spec', 'fixtures', 'terminated_job.json'))} +    end    end  end diff --git a/spec/fixtures/multiple_references_import.zip b/spec/fixtures/multiple_references_import.zipBinary files differ new file mode 100644 index 000000000..28ddff198 --- /dev/null +++ b/spec/fixtures/multiple_references_import.zip diff --git a/spec/fixtures/neptune.zip b/spec/fixtures/neptune.zipBinary files differ deleted file mode 100644 index 86b688b51..000000000 --- a/spec/fixtures/neptune.zip +++ /dev/null diff --git a/spec/fixtures/nozip.zip b/spec/fixtures/nozip.zip new file mode 100644 index 000000000..505bd213a --- /dev/null +++ b/spec/fixtures/nozip.zip @@ -0,0 +1 @@ +no zip file diff --git a/spec/fixtures/ref1.zip b/spec/fixtures/ref1.zipBinary files differ new file mode 100644 index 000000000..1cbd0268e --- /dev/null +++ b/spec/fixtures/ref1.zip diff --git a/spec/fixtures/ref2.zip b/spec/fixtures/ref2.zipBinary files differ new file mode 100644 index 000000000..342353b07 --- /dev/null +++ b/spec/fixtures/ref2.zip diff --git a/spec/fixtures/single_reference_import.zip b/spec/fixtures/single_reference_import.zipBinary files differ new file mode 100644 index 000000000..4aee23614 --- /dev/null +++ b/spec/fixtures/single_reference_import.zip diff --git a/spec/lib/result_spec.rb b/spec/lib/result_spec.rb new file mode 100644 index 000000000..949de163c --- /dev/null +++ b/spec/lib/result_spec.rb @@ -0,0 +1,20 @@ +RSpec.describe Result do + +  context 'is a wrapper of a value' do  +    it { expect( described_class.ok('hello').value ).to eq('hello') } +    it { expect( described_class.error('hello').value ).to eq('hello') } +  end + +  context 'it has status information' do  +    it { expect( described_class.ok('hello') ).to be_ok } +    it { expect( described_class.ok('hello').status ).to eq(:ok) } + +    it { expect( described_class.error('hello') ).not_to be_ok } +    it { expect( described_class.error('hello').status ).to eq(:error) } +  end +   +  context 'nil is just another value' do +    it { expect( described_class.ok(nil) ).to be_ok } +    it { expect( described_class.ok(nil).value ).to be_nil } +  end +end diff --git a/spec/models/api/v1/api_key_spec.rb b/spec/models/api/v1/api_key_spec.rb index 8a34c9221..5f39a65e4 100644 --- a/spec/models/api/v1/api_key_spec.rb +++ b/spec/models/api/v1/api_key_spec.rb @@ -1,11 +1,34 @@  describe Api::V1::ApiKey, :type => :model do -  let!(:referential){create(:referential)} -  subject { Api::V1::ApiKey.create( :name => "test", :referential => referential)} -  it "test" do -    expect(subject).to be_valid +  let(:referential){ create :referential } + +  subject { described_class.create( :name => "test", :referential => referential)} + +  it "validity test" do +    expect_it.to be_valid      expect(subject.referential).to eq(referential) +  end + +  context 'Creation' do +    let( :name ){ SecureRandom.urlsafe_base64 } + +    it 'can be created from a referential with a name, iff needed' do +      # 1st time create a new record +      expect{ described_class.from(referential, name: name) }.to change{ described_class.count }.by(1) +      expect( described_class.last.attributes.values_at(*%w{referential_id name}) ).to eq([ +        referential.id, name +      ]) + +      # 2nd time get the same record +      expect{ described_class.from(referential, name: name) }.not_to change{ described_class.count } +      expect( described_class.last.attributes.values_at(*%w{referential_id name}) ).to eq([ +        referential.id, name +      ]) +    end +    it 'cannot be created without a referential' do +      expect{ described_class.from(nil, name:name) rescue nil }.not_to change{ described_class.count } +    end    end  end diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb index 34bfb0b23..c56858b44 100644 --- a/spec/models/import_spec.rb +++ b/spec/models/import_spec.rb @@ -1,11 +1,12 @@ -require 'rails_helper' -  RSpec.describe Import, :type => :model do +    it { should belong_to(:referential) }    it { should belong_to(:workbench) }    it { should belong_to(:parent).class_name(described_class.to_s) } -  it { should enumerize(:status).in(:new, :pending, :successful, :failed, :canceled, :running, :aborted ) } +  it { should enumerize(:status).in("aborted", "canceled", "failed", "new", "pending", "running", "successful") }    it { should validate_presence_of(:file) } +  it { should validate_presence_of(:referential) } +  it { should validate_presence_of(:workbench) }  end diff --git a/spec/models/organisation_spec.rb b/spec/models/organisation_spec.rb index 527f71015..b16324a56 100644 --- a/spec/models/organisation_spec.rb +++ b/spec/models/organisation_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' -  describe Organisation, :type => :model do    it { should validate_presence_of(:name) }    it { should validate_uniqueness_of(:code) } @@ -17,7 +15,7 @@ describe Organisation, :type => :model do      let(:conf) { Rails.application.config.stif_portail_api }      before :each do        stub_request(:get, "#{conf[:url]}/api/v1/organizations"). -      with(headers: { 'Authorization' => "Token token=\"#{conf[:key]}\"" }). +      with(stub_headers(authorization_token: conf[:key])).        to_return(body: File.open(File.join(Rails.root, 'spec', 'fixtures', 'organizations.json')), status: 200)      end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3a9ae37e9..51ccfccd3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -67,7 +67,7 @@ RSpec.describe User, :type => :model do      let(:conf) { Rails.application.config.stif_portail_api }      before :each do        stub_request(:get, "#{conf[:url]}/api/v1/users"). -        with(headers: { 'Authorization' => "Token token=\"#{conf[:key]}\"" }). +        with(stub_headers(authorization_token: conf[:key])).          to_return(body: File.open(File.join(Rails.root, 'spec', 'fixtures', 'users.json')), status: 200)      end diff --git a/spec/requests/api/v1/netex_import_spec.rb b/spec/requests/api/v1/netex_import_spec.rb index 9fbf8f801..ab1e7f6ae 100644 --- a/spec/requests/api/v1/netex_import_spec.rb +++ b/spec/requests/api/v1/netex_import_spec.rb @@ -4,7 +4,7 @@ RSpec.describe "NetexImport", type: :request do      let( :referential ){ create :referential } -    let( :file_path ){'spec/fixtures/neptune.zip'} +    let( :file_path ){ fixtures_path 'single_reference_import.zip' }      let( :file ){ fixture_file_upload( file_path ) }      let( :post_request ) do @@ -19,7 +19,8 @@ RSpec.describe "NetexImport", type: :request do        {          name: 'hello world',          file: file, -        referential_id: referential.id +        referential_id: referential.id, +        workbench_id:   referential.workbench_id        }      end  diff --git a/spec/services/file_service_spec.rb b/spec/services/file_service_spec.rb new file mode 100644 index 000000000..4426ee145 --- /dev/null +++ b/spec/services/file_service_spec.rb @@ -0,0 +1,17 @@ +# TODO: Delete me after stable implementation of #1726 +# RSpec.describe FileService do + +#   it 'computes a unique filename' do +#     expect( File ).to receive(:exists?).with('xxx/yyy_0').and_return( false ) + +#     expect(described_class.unique_filename('xxx/yyy')).to eq('xxx/yyy_0') +#   end + +#   it 'handles duplicate names by means of a counter' do +#     expect( File ).to receive(:exists?).with('xxx/yyy_0').and_return( true ) +#     expect( File ).to receive(:exists?).with('xxx/yyy_1').and_return( true ) +#     expect( File ).to receive(:exists?).with('xxx/yyy_2').and_return( false ) + +#     expect(described_class.unique_filename('xxx/yyy')).to eq('xxx/yyy_2') +#   end +# end diff --git a/spec/services/http_service_spec.rb b/spec/services/http_service_spec.rb new file mode 100644 index 000000000..8c8af480c --- /dev/null +++ b/spec/services/http_service_spec.rb @@ -0,0 +1,74 @@ +RSpec.describe HTTPService do + +  subject{ described_class } + +  %i{host params path result}.each do |param| +    let(param){ double(param) } +  end +  let( :token ){ SecureRandom.hex } + +  let( :faraday_connection ){ double('faraday_connection') } +  let( :headers ){ {} } + + +  context 'get_resource' do +    let( :params ){ double('params') } + +    it 'sets authorization and returns result' do +      expect(Faraday).to receive(:new).with(url: host).and_yield(faraday_connection) +      expect(faraday_connection).to receive(:adapter).with(Faraday.default_adapter) +      expect(faraday_connection).to receive(:headers).and_return headers +      expect(faraday_connection).to receive(:get).with(path, params).and_return(result) + +      expect(subject.get_resource(host: host, path: path, token: token, params: params)).to eq(result) +      expect(headers['Authorization']).to eq( "Token token=#{token.inspect}" ) +    end +  end + +  context 'post_resource' do +    %i{as_name  mime_type name upload_io value}.each do | param | +      let( param ){ double(param) } +    end + +    let( :upload_list ){ [value, mime_type, as_name] } + +    it 'sets authorization and posts data' do +      expect(Faraday::UploadIO).to receive(:new).with(*upload_list).and_return upload_io +      expect(params).to receive(:update).with(name => upload_io) + +      expect(Faraday).to receive(:new).with(url: host).and_yield(faraday_connection) +      expect(faraday_connection).to receive(:adapter).with(Faraday.default_adapter) +      expect(faraday_connection).to receive(:headers).and_return headers +      expect(faraday_connection).to receive(:request).with(:multipart) +      expect(faraday_connection).to receive(:request).with(:url_encoded) + +      expect(faraday_connection).to receive(:post).with(path, params).and_return(result) + +      expect(subject.post_resource( +        host: host, +        path: path, +        token: token, +        params: params, +        upload: {name => upload_list} )).to eq(result) +      expect(headers['Authorization']).to eq( "Token token=#{token.inspect}" ) +    end + +  end + +  context 'get_json_resource' do + +    let( :content ){ SecureRandom.hex } + +    it 'delegates an parses the response' do +      expect_it.to receive(:get_resource) +        .with(host: host, path: path, token: token, params: params) +        .and_return(double(body: {content: content}.to_json, status: 200)) + +      expect( subject.get_json_resource( +        host: host, +        path: path, +        token: token, +        params: params) ).to eq('content' => content) +    end +  end +end diff --git a/spec/services/retry_service_spec.rb b/spec/services/retry_service_spec.rb new file mode 100644 index 000000000..bb3416373 --- /dev/null +++ b/spec/services/retry_service_spec.rb @@ -0,0 +1,137 @@ +RSpec.describe RetryService do +  subject { described_class.new delays: [2, 3], rescue_from: [NameError, ArgumentError] } + +  context 'no retry necessary' do +    before do +      expect( subject ).not_to receive(:sleep) +    end + +    it 'returns an ok result' do +      expect( subject.execute { 42 } ).to eq(Result.ok(42)) +    end +    it 'does not fail on nil' do +      expect( subject.execute { nil } ).to eq(Result.ok(nil)) +    end + +    it 'fails wihout retries if raising un unregistered exception' do +      expect{ subject.execute{ raise KeyError } }.to raise_error(KeyError) +    end + +  end + +  context 'all retries fail' do +    before do +      expect( subject ).to receive(:sleep).with(2) +      expect( subject ).to receive(:sleep).with(3) +    end +    it 'fails after raising a registered exception n times' do +      result = subject.execute{ raise ArgumentError } +      expect( result.status ).to eq(:error) +      expect( result.value ).to be_kind_of(ArgumentError) +    end +    it 'fails with an explicit try again (automatically registered exception)'  do +      result = subject.execute{ raise RetryService::Retry } +      expect( result.status ).to eq(:error) +      expect( result.value ).to be_kind_of(RetryService::Retry) +    end +  end + +  context "if at first you don't succeed" do +    before do +      @count = 0 +      expect( subject ).to receive(:sleep).with(2) +    end + +    it 'succeeds the second time' do +      expect( subject.execute{ succeed_later(ArgumentError){ 42 } } ).to eq(Result.ok(42)) +    end + +    it 'succeeds the second time with try again (automatically registered exception)' do +      expect( subject.execute{ succeed_later(RetryService::Retry){ 42 } } ).to eq(Result.ok(42)) +    end +  end + +  context 'last chance' do +    before do +      @count = 0 +      expect( subject ).to receive(:sleep).with(2) +      expect( subject ).to receive(:sleep).with(3) +    end +    it 'succeeds the third time with try again (automatically registered exception)' do +      result = subject.execute{ succeed_later(RetryService::Retry, count: 2){ 42 } }   +      expect( result ).to eq( Result.ok(42) ) +    end +  end + +  context 'failure callback once' do  +    subject do  +      described_class.new delays: [2, 3], rescue_from: [NameError, ArgumentError] do |reason, count| +        @reason=reason +        @callback_count=count +        @failures += 1  +      end +    end + +    before do +      @failures = 0 +      @count    = 0 +      expect( subject ).to receive(:sleep).with(2) +    end + +    it 'succeeds the second time and calls the failure_callback once' do +      subject.execute{ succeed_later(RetryService::Retry){ 42 } } +      expect( @failures ).to eq(1) +    end +    it '... and the failure is passed into the callback' do +      subject.execute{ succeed_later(RetryService::Retry){ 42 } } +      expect( @reason ).to be_a(RetryService::Retry) +      expect( @callback_count ).to eq(1) +    end +  end + +  context 'failure callback twice' do +    subject do  +      described_class.new delays: [2, 3], rescue_from: [NameError, ArgumentError] do |_reason, _count| +        @failures += 1  +      end +    end + +    before do +      @failures = 0 +      @count    = 0 +      expect( subject ).to receive(:sleep).with(2) +      expect( subject ).to receive(:sleep).with(3) +    end + +    it 'succeeds the third time and calls the failure_callback twice' do +      subject.execute{ succeed_later(NameError, count: 2){ 42 } } +      expect( @failures ).to eq(2) +    end +  end + +  context 'failure callback in constructor' do +    subject do +      described_class.new(delays: [1, 2], &method(:add2failures)) +    end +    before do +      @failures = [] +      @count    = 0 +      expect( subject ).to receive(:sleep).with(1) +      expect( subject ).to receive(:sleep).with(2) +    end +    it 'succeeds the second time and calls the failure_callback once' do +      subject.execute{ succeed_later(RetryService::Retry, count: 2){ 42 } } +      expect( @failures ).to eq([1,2]) +    end +  end + +  def add2failures( e, c) +    @failures << c  +  end + +  def succeed_later error, count: 1, &blk +    return blk.() unless @count < count +    @count += 1 +    raise error, 'error' +  end +end diff --git a/spec/services/zip_service/zip_entry_data_spec.rb b/spec/services/zip_service/zip_entry_data_spec.rb new file mode 100644 index 000000000..2a7226eb4 --- /dev/null +++ b/spec/services/zip_service/zip_entry_data_spec.rb @@ -0,0 +1,32 @@ +RSpec.describe ZipService do + +  subject{ described_class.new(read_fixture('multiple_references_import.zip')) } + +  it 'can group all entries' do +    expect( subject.entry_groups.keys ).to eq(%w{ref1 ref2}) +  end + +  context 'creates correct zip data for each subdir' do +    it 'e.g. reference1' do +      reference1_stream = subject.entry_group_streams['ref1'] +      control_stream = Zip::InputStream.open( reference1_stream ) +      control_entries = described_class.entries(control_stream) +      expect( control_entries.map{ |e| [e.name, e.get_input_stream.read]}.force ).to eq([ +        ["multiref/ref1/", ""], +        ["multiref/ref1/datum-1", "multi-ref1-datum1\n"], +        ["multiref/ref1/datum-2", "multi-ref1-datum2\n"] +      ]) +    end +    it 'e.g. reference2' do +      reference2_stream = subject.entry_group_streams['ref2'] +      control_stream = Zip::InputStream.open( reference2_stream ) +      control_entries = described_class.entries(control_stream) +      expect( control_entries.map{ |e| [e.name, e.get_input_stream.read]}.force ).to eq([ +        ["multiref/ref2/", ""], +        ["multiref/ref2/datum-1", "multi-ref2-datum1\n"], +        ["multiref/ref2/datum-2", "multi-ref2-datum2\n"] +      ]) +    end +  end + +end diff --git a/spec/services/zip_service/zip_entry_dirs_spec.rb b/spec/services/zip_service/zip_entry_dirs_spec.rb new file mode 100644 index 000000000..8ca1b0f1a --- /dev/null +++ b/spec/services/zip_service/zip_entry_dirs_spec.rb @@ -0,0 +1,33 @@ +RSpec.describe ZipService do + +  let( :zip_service ){ described_class } + +  let( :zip_data ){ File.read zip_file } + +  shared_examples_for 'a correct zip entry reader' do +    it 'gets all entries of the zip file' do +      expect( zip_service.new(zip_data).entry_groups.keys ).to eq(expected) +    end +  end + +  context 'single entry' do +    let( :zip_file ){ fixtures_path 'multiple_references_import.zip' } +    let( :expected ){ %w{ref1 ref2} } + +    it_behaves_like 'a correct zip entry reader' +  end + +  context 'more entries' do +    let( :zip_file ){ fixtures_path 'single_reference_import.zip' } +    let( :expected ){ %w{ref} } + +    it_behaves_like 'a correct zip entry reader' +  end +   +  context 'illegal file' do +    let( :zip_file ){ fixtures_path 'nozip.zip' } +    let( :expected ){ [] } + +    it_behaves_like 'a correct zip entry reader' +  end +end diff --git a/spec/services/zip_service/zip_output_streams_spec.rb b/spec/services/zip_service/zip_output_streams_spec.rb new file mode 100644 index 000000000..fbc60ae92 --- /dev/null +++ b/spec/services/zip_service/zip_output_streams_spec.rb @@ -0,0 +1,21 @@ +RSpec.describe ZipService do +   +  subject{ described_class.new(read_fixture('multiple_references_import.zip')) } + +   +  it 'can write itself to a file' do +    streams = subject.entry_group_streams +    streams.each do | name, stream | +      File.write("tmp/#{name}.zip", stream.string) +    end +    ref1_lines = %x(unzip -l tmp/ref1.zip).split("\n").grep(%r{multiref/ref}).map(&:strip).map(&:split).map(&:last) +    ref2_lines = %x(unzip -l tmp/ref2.zip).split("\n").grep(%r{multiref/ref}).map(&:strip).map(&:split).map(&:last) + +    expect( ref1_lines ).to eq %w(multiref/ref1/ multiref/ref1/datum-1 multiref/ref1/datum-2) +    expect( ref2_lines ).to eq %w(multiref/ref2/ multiref/ref2/datum-1 multiref/ref2/datum-2) +  end + +  it "exposes its size" do +    expect( subject.entry_group_streams.size ).to eq(2) +  end +end diff --git a/spec/support/fixtures_helper.rb b/spec/support/fixtures_helper.rb new file mode 100644 index 000000000..20963261b --- /dev/null +++ b/spec/support/fixtures_helper.rb @@ -0,0 +1,18 @@ +module Support +  module FixturesHelper +    def fixtures_path *segments +      Rails.root.join( fixture_path, *segments ) +    end + +    def open_fixture *segments +      File.open(fixtures_path(*segments)) +    end +    def read_fixture *segments +      File.read(fixtures_path(*segments)) +    end +  end +end + +RSpec.configure do |c| +  c.include Support::FixturesHelper +end diff --git a/spec/support/webmock/helpers.rb b/spec/support/webmock/helpers.rb new file mode 100644 index 000000000..fc6c77850 --- /dev/null +++ b/spec/support/webmock/helpers.rb @@ -0,0 +1,18 @@ +module Support +  module Webmock +    module Helpers +      def stub_headers(*args) +        {headers: make_headers(*args)} +      end + +      def make_headers(headers={}, authorization_token:) +        headers.merge('Authorization' => "Token token=#{authorization_token.inspect}") +      end +    end +  end +end + +RSpec.configure do | conf | +  conf.include Support::Webmock::Helpers, type: :model +  conf.include Support::Webmock::Helpers, type: :worker +end diff --git a/spec/tasks/reflex_rake_spec.rb b/spec/tasks/reflex_rake_spec.rb index 04c5886aa..6ece223d2 100644 --- a/spec/tasks/reflex_rake_spec.rb +++ b/spec/tasks/reflex_rake_spec.rb @@ -5,7 +5,7 @@ describe 'reflex:sync' do      before(:each) do        ['getOP', 'getOR'].each do |method|          stub_request(:get, "#{Rails.application.config.reflex_api_url}/?format=xml&idRefa=0&method=#{method}"). -        to_return(body: File.open("#{fixture_path}/reflex.zip"), status: 200) +        to_return(body: open_fixture('reflex.zip'), status: 200)        end        stop_area_ref = create(:stop_area_referential, name: 'Reflex') @@ -43,7 +43,7 @@ describe 'reflex:sync' do        before(:each) do          ['getOP', 'getOR'].each do |method|            stub_request(:get, "#{Rails.application.config.reflex_api_url}/?format=xml&idRefa=0&method=#{method}"). -          to_return(body: File.open("#{fixture_path}/reflex_updated.zip"), status: 200) +          to_return(body: open_fixture('reflex_updated.zip'), status: 200)          end          Stif::ReflexSynchronization.synchronize        end diff --git a/spec/workers/stop_area_referential_sync_worker_spec.rb b/spec/workers/stop_area_referential_sync_worker_spec.rb index 48b64e55e..50c7cf45f 100644 --- a/spec/workers/stop_area_referential_sync_worker_spec.rb +++ b/spec/workers/stop_area_referential_sync_worker_spec.rb @@ -1,4 +1,3 @@ -require 'rails_helper'  RSpec.describe StopAreaReferentialSyncWorker, type: :worker do    let!(:stop_area_referential_sync) { create :stop_area_referential_sync } diff --git a/spec/workers/workbench_import_worker_spec.rb b/spec/workers/workbench_import_worker_spec.rb new file mode 100644 index 000000000..b719cbb98 --- /dev/null +++ b/spec/workers/workbench_import_worker_spec.rb @@ -0,0 +1,119 @@ +RSpec.describe WorkbenchImportWorker, type: [:worker, :request] do + +  let( :worker ) { described_class.new } +  let( :import ){ build_stubbed :import, token_download: download_token, file: zip_file } + +  let( :workbench ){ import.workbench } +  let( :referential ){ import.referential } +  let( :api_key ){ build_stubbed :api_key, referential: referential, token: "#{referential.id}-#{SecureRandom.hex}" } +  let( :params ) do +    { netex_import: +      { referential_id: referential.id, workbench_id: workbench.id } +    } +  end + +  # http://www.example.com/workbenches/:workbench_id/imports/:id/download +  let( :host ){ Rails.configuration.rails_host } +  let( :path ){ download_workbench_import_path(workbench, import) } + +  let( :downloaded_zip ){ double("downloaded zip") } +  let( :download_zip_response ){ OpenStruct.new( body: downloaded_zip ) } +  let( :download_token ){ SecureRandom.urlsafe_base64 } + + +  let( :upload_path ) { api_v1_netex_imports_path(format: :json) } + +  let( :entry_group_streams ) do +    entry_count.times.map{ |i| double( "entry group stream #{i}" ) } +  end +  let( :entry_groups ) do +    entry_count.times.map do | i | +      {"entry_group_name#{i}" => entry_group_streams[i] } +    end +  end + +  let( :zip_service ){ double("zip service") } +  let( :zip_file ){ open_fixture('multiple_references_import.zip') } + +  let( :post_response_ok ){ double(status: 201, body: "{}") } + +  before do +    # Silence Logger +    allow_any_instance_of(Logger).to receive(:info) +    allow_any_instance_of(Logger).to receive(:warn) + +    # That should be `build_stubbed's` job, no? +    allow(Import).to receive(:find).with(import.id).and_return(import) + +    allow(Api::V1::ApiKey).to receive(:from).and_return(api_key) +    allow(ZipService).to receive(:new).with(downloaded_zip).and_return zip_service +    expect(zip_service).to receive(:entry_group_streams).and_return(entry_groups) +    expect( import ).to receive(:update_attributes).with(status: 'running') +  end + + +  context 'multireferential zipfile, no errors' do +    let( :entry_count ){ 2 } + +    it 'downloads a zip file, cuts it, and uploads all pieces' do + +      expect(HTTPService).to receive(:get_resource) +        .with(host: host, path: path, params: {token: download_token}) +        .and_return( download_zip_response ) + +      entry_groups.each do | entry_group_name, entry_group_stream | +        mock_post entry_group_name, entry_group_stream, post_response_ok +      end + +      expect( import ).to receive(:update_attributes).with(total_steps: 2) +      expect( import ).to receive(:update_attributes).with(current_step: 1) +      expect( import ).to receive(:update_attributes).with(current_step: 2) + +      worker.perform import.id + +    end +  end + +  context 'multireferential zipfile with error' do +    let( :entry_count ){ 3 } +    let( :post_response_failure ){ double(status: 406, body: {error: 'What was you thinking'}) } + +    it 'downloads a zip file, cuts it, and uploads some pieces' do +      expect(HTTPService).to receive(:get_resource) +        .with(host: host, path: path, params: {token: download_token}) +        .and_return( download_zip_response ) + +      # First entry_group succeeds +      entry_groups[0..0].each do | entry_group_name, entry_group_stream | +        mock_post entry_group_name, entry_group_stream, post_response_ok +      end + +      # Second entry_group fails (M I S E R A B L Y) +      entry_groups[1..1].each do | entry_group_name, entry_group_stream | +        mock_post entry_group_name, entry_group_stream, post_response_failure +        WorkbenchImportWorker::RETRY_DELAYS.each do | delay | +          mock_post entry_group_name, entry_group_stream, post_response_failure +          expect_any_instance_of(RetryService).to receive(:sleep).with(delay) +        end +      end + +      expect( import ).to receive(:update_attributes).with(total_steps: 3) +      expect( import ).to receive(:update_attributes).with(current_step: 1) +      expect( import ).to receive(:update_attributes).with(current_step: 2) +      expect( import ).to receive(:update_attributes).with(current_step: 3, status: 'failed') + +      worker.perform import.id + +    end +  end + +  def mock_post entry_group_name, entry_group_stream, response +    expect( HTTPService ).to receive(:post_resource) +      .with(host: host, +            path: upload_path, +            token: api_key.token, +            params: params, +            upload: {file: [entry_group_stream, 'application/zip', entry_group_name]}) +      .and_return(response) +  end +end | 
