diff options
62 files changed, 613 insertions, 115 deletions
| diff --git a/Gemfile.lock b/Gemfile.lock index da7c4f3c1..3acc27613 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -256,7 +256,8 @@ GEM      htmlbeautifier (1.3.1)      httparty (0.14.0)        multi_xml (>= 0.5.2) -    i18n (0.8.6) +    i18n (0.9.0) +      concurrent-ruby (~> 1.0)      i18n-tasks (0.9.15)        activesupport (>= 4.0.2)        ast (>= 2.1.0) @@ -380,7 +381,7 @@ GEM        activesupport (>= 4.2.0.beta, < 5.0)        nokogiri (~> 1.6)        rails-deprecated_sanitizer (>= 1.0.1) -    rails-erd (1.5.0) +    rails-erd (1.5.2)        activerecord (>= 3.2)        activesupport (>= 3.2)        choice (~> 0.2.0) diff --git a/INSTALL.md b/INSTALL.md index 45eb030ff..45624e30b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -19,6 +19,14 @@ Go into your local repro and install the gems          bundle +## Node and Yarn + +Yarn needs a node version ≥ 6, if you use Node Version Manager [NVM](https://github.com/creationix/nvm)  you can rely on the content of `.nvmrc`. + +Otherwise please make sure to use a compatible version, still best to use the same as indicated by `.nvrmc`. + +Then install yarn (`brew install yarn` does nicely on macOS). +  ### Installation Caveats  #### Node Related Issue, libv8 diff --git a/app/assets/stylesheets/components/_tables.sass b/app/assets/stylesheets/components/_tables.sass index 8fe7be374..178ec2f36 100644 --- a/app/assets/stylesheets/components/_tables.sass +++ b/app/assets/stylesheets/components/_tables.sass @@ -247,6 +247,16 @@        width: 35px        height: 35px        margin: 5px +      &.with_text +        width: initial +        > a, > button +          border-radius: 4% +          text-decoration: none +        span +          &.fa +            padding-left: 10px +        span +          padding: 0 10px 0 0        > a, > button          display: block diff --git a/app/controllers/api/v1/imports_controller.rb b/app/controllers/api/v1/imports_controller.rb index 6050418d8..3d7f4ca79 100644 --- a/app/controllers/api/v1/imports_controller.rb +++ b/app/controllers/api/v1/imports_controller.rb @@ -5,7 +5,11 @@ class Api::V1::ImportsController < Api::V1::IbooController    def create      args    = workbench_import_params.merge(creator: 'Webservice')      @import = parent.workbench_imports.create(args) -    create! +    if @import.valid?  +      create! +    else +      render json: { status: "error", messages: @import.errors.full_messages } +    end    end    private diff --git a/app/controllers/compliance_control_sets_controller.rb b/app/controllers/compliance_control_sets_controller.rb index 9de90c21c..e5c2e19dd 100644 --- a/app/controllers/compliance_control_sets_controller.rb +++ b/app/controllers/compliance_control_sets_controller.rb @@ -23,6 +23,12 @@ class ComplianceControlSetsController < BreadcrumbController      end    end +  def clone +    ComplianceControlSetCloner.new.copy(params[:id], current_organisation.id) +    flash[:notice] = I18n.t("compliance_control_sets.errors.operation_in_progress") +    redirect_to(compliance_control_sets_path) +  end +    protected    def begin_of_association_chain @@ -48,4 +54,4 @@ class ComplianceControlSetsController < BreadcrumbController    def compliance_control_set_params      params.require(:compliance_control_set).permit(:name, :id)    end -end +end
\ No newline at end of file diff --git a/app/controllers/compliance_controls_controller.rb b/app/controllers/compliance_controls_controller.rb index e51d1eb74..0a9c7d52b 100644 --- a/app/controllers/compliance_controls_controller.rb +++ b/app/controllers/compliance_controls_controller.rb @@ -1,6 +1,7 @@  class ComplianceControlsController < BreadcrumbController    defaults resource_class: ComplianceControl    belongs_to :compliance_control_set +  actions :all, :except => [:show, :index]    def select_type      @sti_subclasses = ComplianceControl.subclasses @@ -15,7 +16,6 @@ class ComplianceControlsController < BreadcrumbController    end    def create -    puts build_resource.inspect      create! do |success, failure|        success.html { redirect_to compliance_control_set_path(parent) }        failure.html { render( :action => 'new' ) } diff --git a/app/controllers/time_tables_controller.rb b/app/controllers/time_tables_controller.rb index 2d24d5aa6..af74f635f 100644 --- a/app/controllers/time_tables_controller.rb +++ b/app/controllers/time_tables_controller.rb @@ -130,7 +130,6 @@ class TimeTablesController < ChouetteController      scope = select_time_tables      if params[:q] && params[:q]["tag_search"]        tags = params[:q]["tag_search"].reject {|c| c.empty?} -      params[:q].delete("tag_search")        scope = select_time_tables.tagged_with(tags, :any => true) if tags.any?      end      scope = self.ransack_period_range(scope: scope, error_message: t('referentials.errors.validity_period'), query: :overlapping) diff --git a/app/decorators/compliance_control_set_decorator.rb b/app/decorators/compliance_control_set_decorator.rb index f4aa607e1..7515316ce 100644 --- a/app/decorators/compliance_control_set_decorator.rb +++ b/app/decorators/compliance_control_set_decorator.rb @@ -4,6 +4,13 @@ class ComplianceControlSetDecorator < Draper::Decorator    def action_links      links = [] +    # if policy.clone? +      links << Link.new( +        content: h.t('actions.clone'), +        href: h.clone_compliance_control_set_path(object.id) +      ) +    # end +      # if h.policy(object).destroy?        links << Link.new(          content: h.destroy_link_content, diff --git a/app/models/compliance_control.rb b/app/models/compliance_control.rb index baf491e8a..08efa7e9a 100644 --- a/app/models/compliance_control.rb +++ b/app/models/compliance_control.rb @@ -24,7 +24,6 @@ class ComplianceControl < ActiveRecord::Base    end    class << self -    def default_criticity; :warning end      def default_code; "" end      def dynamic_attributes        hstore_metadata_for_control_attributes.keys diff --git a/app/models/generic_attribute_control/min_max.rb b/app/models/generic_attribute_control/min_max.rb index a83ab64ab..ab6f546a7 100644 --- a/app/models/generic_attribute_control/min_max.rb +++ b/app/models/generic_attribute_control/min_max.rb @@ -9,7 +9,6 @@ module GenericAttributeControl      class << self        def attribute_type; :integer end -      def default_criticity; :warning end        def default_code; "3-Generic-2" end      end    end diff --git a/app/models/generic_attribute_control/pattern.rb b/app/models/generic_attribute_control/pattern.rb index 027d6948e..3a4a55d5c 100644 --- a/app/models/generic_attribute_control/pattern.rb +++ b/app/models/generic_attribute_control/pattern.rb @@ -7,7 +7,6 @@ module GenericAttributeControl      class << self        def attribute_type; :string end -      def default_criticity; :warning end        def default_code; "3-Generic-1" end      end    end diff --git a/app/models/generic_attribute_control/uniqueness.rb b/app/models/generic_attribute_control/uniqueness.rb index 36a270d74..f707c944b 100644 --- a/app/models/generic_attribute_control/uniqueness.rb +++ b/app/models/generic_attribute_control/uniqueness.rb @@ -6,7 +6,6 @@ module GenericAttributeControl      class << self        def attribute_type; :string end -      def default_criticity; :warning end        def default_code; "3-Generic-3" end      end    end diff --git a/app/models/import.rb b/app/models/import.rb index 64f713914..4ff8326ab 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -18,6 +18,7 @@ class Import < ActiveRecord::Base    validates :file, presence: true    validates_presence_of :workbench, :creator +  validates_format_of :file, with: %r{\.zip\z}i, message: I18n.t('activerecord.errors.models.imports.wrong_file_extension')    before_create :initialize_fields diff --git a/app/models/route_control/minimum_length.rb b/app/models/route_control/minimum_length.rb index 56becfb2b..7aee4da21 100644 --- a/app/models/route_control/minimum_length.rb +++ b/app/models/route_control/minimum_length.rb @@ -1,5 +1,6 @@  module RouteControl    class MinimumLength < ComplianceControl +    enumerize :criticity, in: %i(error), scope: true, default: :error      def self.default_code; "3-Route-6" end    end diff --git a/app/models/route_control/opposite_route.rb b/app/models/route_control/opposite_route.rb index 0148087ca..3921bb0a2 100644 --- a/app/models/route_control/opposite_route.rb +++ b/app/models/route_control/opposite_route.rb @@ -1,5 +1,6 @@  module RouteControl    class OppositeRoute < ComplianceControl +    enumerize :criticity, in: %i(error), scope: true, default: :error      def self.default_code; "3-Route-2" end    end diff --git a/app/models/vehicle_journey_control/time_table.rb b/app/models/vehicle_journey_control/time_table.rb index 617ead12b..1e3166693 100644 --- a/app/models/vehicle_journey_control/time_table.rb +++ b/app/models/vehicle_journey_control/time_table.rb @@ -1,5 +1,6 @@  module VehicleJourneyControl    class TimeTable < ComplianceControl +    enumerize :criticity, in: %i(error), scope: true, default: :error      def self.default_code; "3-VehicleJourney-4" end    end diff --git a/app/models/vehicle_journey_control/vehicle_journey_at_stops.rb b/app/models/vehicle_journey_control/vehicle_journey_at_stops.rb index 47ee26f82..392e7764f 100644 --- a/app/models/vehicle_journey_control/vehicle_journey_at_stops.rb +++ b/app/models/vehicle_journey_control/vehicle_journey_at_stops.rb @@ -1,5 +1,6 @@  module VehicleJourneyControl    class VehicleJourneyAtStops < ComplianceControl +    enumerize :criticity, in: %i(error), scope: true, default: :error      def self.default_code; "3-VehicleJourney-5" end    end diff --git a/app/services/zip_service.rb b/app/services/zip_service.rb index cab301b01..7a4bdad1b 100644 --- a/app/services/zip_service.rb +++ b/app/services/zip_service.rb @@ -1,10 +1,9 @@  class ZipService -  # TODO: Remove me before merge https://github.com/rubyzip/rubyzip -  class Subdir < Struct.new(:name, :stream) +  class Subdir < Struct.new(:name, :stream, :spurious)    end -  attr_reader :current_key, :current_output, :yielder +  attr_reader :current_key, :current_output, :current_spurious, :yielder    def initialize data      @zip_data       = StringIO.new(data) @@ -36,6 +35,7 @@ class ZipService    end    def add_to_current_output entry +    return if is_spurious! entry.name      current_output.put_next_entry entry.name      write_to_current_output entry.get_input_stream    end @@ -51,7 +51,8 @@ class ZipService        @yielder  << Subdir.new(          current_key,          # Second part of the solution, yield the closed stream -        current_output.close_buffer) +        current_output.close_buffer, +        current_spurious)      end    end @@ -59,10 +60,19 @@ class ZipService      @current_key    = entry_key      # First piece of the solution, use internal way to create a Zip::OutputStream      @current_output = Zip::OutputStream.new(StringIO.new(''), true, nil) +    @current_spurious = []    end    def entry_key entry      # last dir name File.dirname.split("/").last      entry.name.split('/', -1)[-2]    end + +  def is_spurious! entry_name +    segments = entry_name.split('/', 3) +    return false if segments.size < 3 + +    current_spurious << segments.second +    return true +  end  end diff --git a/app/views/compliance_control_blocks/edit.html.slim b/app/views/compliance_control_blocks/edit.html.slim index 0ac507ece..637bb7311 100644 --- a/app/views/compliance_control_blocks/edit.html.slim +++ b/app/views/compliance_control_blocks/edit.html.slim @@ -1,5 +1,5 @@  / PageHeader -= pageheader 'modele-calendrier', += pageheader 'jeux-de-controle',          t('compliance_control_blocks.edit.title', compliance_control_block: @compliance_control_block.id) diff --git a/app/views/compliance_control_blocks/new.html.slim b/app/views/compliance_control_blocks/new.html.slim index 654a0dc7f..49404c552 100644 --- a/app/views/compliance_control_blocks/new.html.slim +++ b/app/views/compliance_control_blocks/new.html.slim @@ -1,5 +1,5 @@  / PageHeader -= pageheader 'modele-calendrier', += pageheader 'jeux-de-controle',          t('compliance_control_blocks.new.title') diff --git a/app/views/compliance_control_sets/edit.html.slim b/app/views/compliance_control_sets/edit.html.slim index 934bd81b0..649154b91 100644 --- a/app/views/compliance_control_sets/edit.html.slim +++ b/app/views/compliance_control_sets/edit.html.slim @@ -1,5 +1,5 @@  / PageHeader -= pageheader 'modele-calendrier', += pageheader 'jeux-de-controle',               t('compliance_control_sets.edit.title', name: @compliance_control_set.name)  / PageContent diff --git a/app/views/compliance_control_sets/index.html.slim b/app/views/compliance_control_sets/index.html.slim index 68173fee9..1120ed186 100644 --- a/app/views/compliance_control_sets/index.html.slim +++ b/app/views/compliance_control_sets/index.html.slim @@ -1,5 +1,5 @@  / PageHeader -- header_params = ['jeux-de-donnees', +- header_params = ['jeux-de-controle',          t('compliance_control_sets.index.title'),          '']  - header_params << link_to(t('compliance_control_sets.actions.new'), new_compliance_control_set_path, class: 'btn btn-default')  if policy(Calendar).create? diff --git a/app/views/compliance_control_sets/new.html.slim b/app/views/compliance_control_sets/new.html.slim index d6be41ee8..35654b4d6 100644 --- a/app/views/compliance_control_sets/new.html.slim +++ b/app/views/compliance_control_sets/new.html.slim @@ -1,5 +1,5 @@  / PageHeader -= pageheader 'modele-calendrier', += pageheader 'jeux-de-controle',          t('compliance_control_sets.index.new') diff --git a/app/views/compliance_control_sets/show.html.slim b/app/views/compliance_control_sets/show.html.slim index b6e203a9e..f45b0227a 100644 --- a/app/views/compliance_control_sets/show.html.slim +++ b/app/views/compliance_control_sets/show.html.slim @@ -1,5 +1,5 @@  / PageHeader -= pageheader 'jeux-de-donnees', += pageheader 'jeux-de-controle',                t('compliance_control_sets.show.title', name: @compliance_control_set.name),               'Lorem ipsum dolor sit amet' @@ -96,9 +96,13 @@                  cls: 'table has-filter has-search'      .select_toolbox        ul -        li.st_action +        li.st_action.with_text            = link_to select_type_compliance_control_set_compliance_controls_path(@compliance_control_set.id)              span.fa.fa-plus -        li.st_action +            span +              = t('compliance_control_sets.actions.add_compliance_control') +        li.st_action.with_text            = link_to new_compliance_control_set_compliance_control_block_path(@compliance_control_set.id) -            span.fa.fa-plus-square +            span.fa.fa-plus +            span +              = t('compliance_control_sets.actions.add_compliance_control_block') diff --git a/app/views/compliance_controls/edit.html.slim b/app/views/compliance_controls/edit.html.slim index d7497c0e2..1d478e845 100644 --- a/app/views/compliance_controls/edit.html.slim +++ b/app/views/compliance_controls/edit.html.slim @@ -1,4 +1,4 @@ -= pageheader 'compliance-control', += pageheader 'jeux-de-controle',          t('compliance_controls.edit.title') diff --git a/app/views/compliance_controls/new.html.slim b/app/views/compliance_controls/new.html.slim index 962f70ecc..181f49a15 100644 --- a/app/views/compliance_controls/new.html.slim +++ b/app/views/compliance_controls/new.html.slim @@ -1,5 +1,5 @@  / PageHeader -- header_params = ['jeux-de-donnees', +- header_params = ['jeux-de-controle',          t('compliance_controls.new.title'),          '']  = pageheader(*header_params) do diff --git a/app/views/compliance_controls/select_type.html.slim b/app/views/compliance_controls/select_type.html.slim index c6e3b0427..98cc5a943 100644 --- a/app/views/compliance_controls/select_type.html.slim +++ b/app/views/compliance_controls/select_type.html.slim @@ -1,5 +1,5 @@  / PageHeader -- header_params = ['jeux-de-donnees', +- header_params = ['jeux-de-controle',          t('compliance_controls.select_type.title'),          '']  = pageheader(*header_params) do diff --git a/app/views/compliance_controls/show.html.slim b/app/views/compliance_controls/show.html.slim index 8232dbe28..12a28cd4b 100644 --- a/app/views/compliance_controls/show.html.slim +++ b/app/views/compliance_controls/show.html.slim @@ -1,7 +1,9 @@  / PageHeader -= pageheader 'jeux-de-donnees', += pageheader 'jeux-de-controle',          t('compliance_controls.show.title'), -        '' +        '', +        link_to(t('actions.edit'), edit_compliance_control_set_compliance_control_path(params[:compliance_control_set_id], params[:id]), class: 'btn btn-default') do +  / PageContent  .page_content    .container-fluid diff --git a/app/views/imports/_form.html.slim b/app/views/imports/_form.html.slim index 0fbf578be..95d97c534 100644 --- a/app/views/imports/_form.html.slim +++ b/app/views/imports/_form.html.slim @@ -9,6 +9,6 @@        .form-group          = form.label :file, t('activerecord.attributes.import.resources'), class: 'control-label col-sm-4 col-xs-5'          .col-sm-8.col-xs-7 -          = form.input_field :file, label: false, class: 'form-control' +          = form.input :file, label: false, class: 'form-control'    = form.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'wb_import_form' diff --git a/app/views/time_tables/_filter.html.slim b/app/views/time_tables/_filter.html.slim index 2672f7dfc..11e9987c4 100644 --- a/app/views/time_tables/_filter.html.slim +++ b/app/views/time_tables/_filter.html.slim @@ -9,7 +9,7 @@    .ffg-row      .form-group        = f.label Chouette::TimeTable.human_attribute_name(:tag_search), required: false, class: 'control-label' -      = f.input :tag_search, as: :tags, collection: Chouette::TimeTable.tags_on(:tags).pluck(:name), label: false, input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Indiquez une étiquette...' }, wrapper_html: { class: 'select2ed'}, include_blank: false +      = f.input :tag_search, as: :tags, collection: Chouette::TimeTable.tags_on(:tags).pluck(:name), label: false, input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Indiquez une étiquette...' }, wrapper_html: { class: 'select2ed'}, include_blank: false, selected: params[:q] ? params[:q]['tag_search'] : nil      .form-group.togglable        = f.label Chouette::TimeTable.human_attribute_name(:bounding_dates), required: false, class: 'control-label' diff --git a/app/workers/compliance_control_set_cloning_worker.rb b/app/workers/compliance_control_set_cloning_worker.rb new file mode 100644 index 000000000..9cbe5c81a --- /dev/null +++ b/app/workers/compliance_control_set_cloning_worker.rb @@ -0,0 +1,8 @@ +class ComplianceControlSetCloningWorker +  include Sidekiq::Worker + +  def perform id, organisation_id +    ComplianceControlSetCloner.new.copy id, organisation_id +  end + +end diff --git a/app/workers/workbench_import_worker.rb b/app/workers/workbench_import_worker.rb index 994493944..300fad9e2 100644 --- a/app/workers/workbench_import_worker.rb +++ b/app/workers/workbench_import_worker.rb @@ -14,11 +14,13 @@ class WorkbenchImportWorker      zip_service = ZipService.new(downloaded)      upload zip_service      @workbench_import.update(ended_at: Time.now) +  rescue Zip::Error +    handle_corrupt_zip_file    end    def download      logger.info  "HTTP GET #{import_url}" -    @zipfile_data = HTTPService.get_resource( +    HTTPService.get_resource(        host: import_host,        path: import_path,        params: {token: @workbench_import.token_download}).body @@ -32,6 +34,10 @@ class WorkbenchImportWorker        params: params(eg_file, eg_name))    end +  def handle_corrupt_zip_file +    @workbench_import.messages.create(criticity: :error, message_key: 'corrupt_zip_file', message_attributes: {import_name: @workbench_import.name}) +  end +    def upload zip_service      entry_group_streams = zip_service.subdirs      @workbench_import.update total_steps: entry_group_streams.size @@ -42,11 +48,24 @@ class WorkbenchImportWorker      raise    end -  def upload_entry_group entry_pair, element_count -    @workbench_import.update( current_step: element_count.succ ) -    # status = retry_service.execute(&upload_entry_group_proc(entry_pair)) -    eg_name = entry_pair.name -    eg_stream = entry_pair.stream +  def update_object_state entry, count +    @workbench_import.update( current_step: count ) +    unless entry.spurious.empty? +      @workbench_import.messages.create( +        criticity: :warning, +        message_key: 'inconsistent_zip_file', +        message_attributes: { +          'import_name' => @workbench_import.name, +          'spurious_dirs' => entry.spurious.join(', ') +        })  +    end +  end + +  def upload_entry_group entry, element_count +    update_object_state entry, element_count.succ +    # status = retry_service.execute(&upload_entry_group_proc(entry)) +    eg_name = entry.name +    eg_stream = entry.stream      FileUtils.mkdir_p(Rails.root.join('tmp', 'imports')) diff --git a/config/initializers/relationship.rb b/config/initializers/relationship.rb new file mode 100644 index 000000000..492aa627f --- /dev/null +++ b/config/initializers/relationship.rb @@ -0,0 +1,17 @@ +if Rails.env.development? +  require 'rails_erd/domain/relationship' + +  module RailsERD +    class Domain +      class Relationship +        class << self +          private + +          def association_identity(association) +            Set[association_owner(association), association_target(association)] +          end +        end +      end +    end +  end +end diff --git a/config/locales/compliance_control_blocks.en.yml b/config/locales/compliance_control_blocks.en.yml index a37b41db5..fbface6b2 100644 --- a/config/locales/compliance_control_blocks.en.yml +++ b/config/locales/compliance_control_blocks.en.yml @@ -10,9 +10,11 @@ fr:          transport_mode: Transport mode          sub_transport_mode: Transport submode    compliance_control_blocks: +    clone: +      prefix: 'Copy of'      actions:        destroy_confirm: Are you sure you want to destroy this block ?      new:        title: Create a control block      edit: -      title: "Edit the control block : %{compliance_control_block}"
\ No newline at end of file +      title: "Edit the control block : %{compliance_control_block}" diff --git a/config/locales/compliance_control_blocks.fr.yml b/config/locales/compliance_control_blocks.fr.yml index f93cafa54..66df008be 100644 --- a/config/locales/compliance_control_blocks.fr.yml +++ b/config/locales/compliance_control_blocks.fr.yml @@ -10,9 +10,11 @@ fr:          transport_mode: Mode de transport          transport_submode: Sous-mode de transport    compliance_control_blocks: +    clone: +      prefix: 'Copie de'      actions:        destroy_confirm: Etes vous sûr de supprimer ce bloc ?      new:        title: Créer un groupe de contrôle(s)      edit: -      title: "Editer le groupe de contrôle : %{compliance_control_block}"
\ No newline at end of file +      title: "Editer le groupe de contrôle : %{compliance_control_block}" diff --git a/config/locales/compliance_control_sets.en.yml b/config/locales/compliance_control_sets.en.yml index 83b14642c..7361edacf 100644 --- a/config/locales/compliance_control_sets.en.yml +++ b/config/locales/compliance_control_sets.en.yml @@ -1,5 +1,9 @@  en:    compliance_control_sets: +    clone: +      prefix: 'Copy of' +    errors: +      operation_in_progress: "The clone operation is in progress. Please wait and refresh the page in a few moments"      index:        title: Compliance control set        new: New compliance control set @@ -11,7 +15,8 @@ en:        edit: Edit        show: Show        destroy: Destroy -      add_compliance_control: Add a compliance control +      add_compliance_control: Compliance Control +      add_compliance_control_block: Compliance Control Block        destroy_confirm: Are you sur ?      filters:        name: 'Enter name ...' diff --git a/config/locales/compliance_control_sets.fr.yml b/config/locales/compliance_control_sets.fr.yml index 37851d7c4..19f6f08ee 100644 --- a/config/locales/compliance_control_sets.fr.yml +++ b/config/locales/compliance_control_sets.fr.yml @@ -1,5 +1,9 @@  fr:    compliance_control_sets: +    clone: +      prefix: 'Copie de' +    errors: +      operation_in_progress: "L'opération de clone est en cours. Veuillez patienter et raffraichir la page dans quelques instants"      index:        title: "Liste des jeux de contrôles"      edit: @@ -13,7 +17,8 @@ fr:        edit: Editer        show: Consulter        destroy: Supprimer -      add_compliance_control: Ajouter un JDC +      add_compliance_control: Contrôle +      add_compliance_control_block: Groupe de contrôles        loaded: Charger le contrôle        destroy_confirm: Etes vous sûr de supprimer ce jeux de contrôle ?      filters: diff --git a/config/locales/compliance_controls.en.yml b/config/locales/compliance_controls.en.yml index 3063c35a4..887bc2009 100644 --- a/config/locales/compliance_controls.en.yml +++ b/config/locales/compliance_controls.en.yml @@ -1,5 +1,7 @@  en:    compliance_controls: +    clone: +      prefix: 'Copy of'      min_max_values: "the minimum (%{min}) is not supposed to be greater than the maximum (%{max})"      errors:        incoherent_control_sets: "Impossible to assign a control to a set (id: %{direct_set_name}) differing from the one of its group (id: %{indirect_set_name})" diff --git a/config/locales/compliance_controls.fr.yml b/config/locales/compliance_controls.fr.yml index 2038b9eb7..2feb201bf 100644 --- a/config/locales/compliance_controls.fr.yml +++ b/config/locales/compliance_controls.fr.yml @@ -1,5 +1,7 @@  fr:    compliance_controls: +    clone: +      prefix: 'Copie de'      min_max_values: "la valeur de minimum (%{min}) ne doit pas être superieur à la valuer du maximum (%{max})"      errors:        incoherent_control_sets: "Le contrôle ne peut pas être associé à un jeu de contrôle (id: %{direct_set_name}) différent de celui de son groupe (id: %{indirect_set_name})" diff --git a/config/locales/import_messages.en.yml b/config/locales/import_messages.en.yml index 4009d7c77..528ab3477 100644 --- a/config/locales/import_messages.en.yml +++ b/config/locales/import_messages.en.yml @@ -1,6 +1,8 @@  en:    import_messages:      compliance_check_messages: +      corrupt_zip_file: "The zip file of WorkbenchImport %{import_name} is corrupted and cannot be read" +      inconsistent_zip_file: "The zip file of WorkbenchImport %{import_name} contains the following spurious directories %{spurious_dirs}, which are ignored"        referential_creation: "Le référentiel n'a pas pu être créé car un référentiel existe déjà sur les même périodes et lignes"        1_netexstif_2: "Le fichier %{source_filename} ne respecte pas la syntaxe XML ou la XSD NeTEx : erreur '%{error_value}' rencontré"        1_netexstif_5: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{source_label} d'identifiant %{source_objectid} a une date de mise à jour dans le futur" diff --git a/config/locales/import_messages.fr.yml b/config/locales/import_messages.fr.yml index 085299bb4..15de6eed8 100644 --- a/config/locales/import_messages.fr.yml +++ b/config/locales/import_messages.fr.yml @@ -1,6 +1,8 @@  fr:    import_messages:      compliance_check_messages: +      corrupt_zip_file: "Le fichier zip du WorkbenchImport %{import_name} est corrompu, et ne peut être lu"  +      inconsistent_zip_file: "Le fichier zip du WorkbenchImport %{import_name} contient les repertoirs illegeaux %{spurious_dirs} qui seront ignorés"        referential_creation: "Le référentiel n'a pas pu être créé car un référentiel existe déjà sur les même périodes et lignes"        1_netexstif_2: "Le fichier %{source_filename} ne respecte pas la syntaxe XML ou la XSD NeTEx : erreur '%{error_value}' rencontré"        1_netexstif_5: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{source_label} d'identifiant %{source_objectid} a une date de mise à jour dans le futur" diff --git a/config/locales/imports.en.yml b/config/locales/imports.en.yml index 9bf877c86..f3bcad9e9 100644 --- a/config/locales/imports.en.yml +++ b/config/locales/imports.en.yml @@ -53,6 +53,10 @@ en:          zero:  "import"          one:   "NeTEx import"          other: "imports" +    errors: +      models: +        imports: +          wrong_file_extension: "The imported file must be a zip file"      attributes:        import:          resources: "File to import" diff --git a/config/locales/imports.fr.yml b/config/locales/imports.fr.yml index 6998c89d2..6e74fa33c 100644 --- a/config/locales/imports.fr.yml +++ b/config/locales/imports.fr.yml @@ -53,6 +53,10 @@ fr:          zero:  "import"          one:   "import NeTEx"          other: "imports" +    errors: +      models: +        imports: +          wrong_file_extension: "Le fichier importé doit être au format zip"      attributes:        import:          resources: "Fichier à importer" diff --git a/config/routes.rb b/config/routes.rb index b9e318f91..b105e77d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,7 @@ ChouetteIhm::Application.routes.draw do    resources :api_keys, :only => [:edit, :update, :new, :create, :destroy]    resources :compliance_control_sets do +    get :clone, on: :member      resources :compliance_controls, except: :index do        get :select_type, on: :collection      end diff --git a/db/schema.rb b/db/schema.rb index 181c83402..a64a426de 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -408,9 +408,9 @@ ActiveRecord::Schema.define(version: 20171016074044) do      t.string   "type"      t.integer  "parent_id",             limit: 8      t.string   "parent_type" +    t.datetime "notified_parent_at"      t.integer  "current_step",                    default: 0      t.integer  "total_steps",                     default: 0 -    t.datetime "notified_parent_at"      t.string   "creator"    end diff --git a/lib/compliance_control_set_cloner.rb b/lib/compliance_control_set_cloner.rb new file mode 100644 index 000000000..12e1eccb5 --- /dev/null +++ b/lib/compliance_control_set_cloner.rb @@ -0,0 +1,93 @@ +class ComplianceControlSetCloner + +  # Naming Convention: As we are in a domain with quite long names we +  #                    abbreviate compliance_control to cc and +  #                    compliance_check to cck iff used as prefixes. + +  attr_reader :organisation_id, :source_set_id +   +  def copy source_set_id, organisation_id +    @source_set_id = source_set_id +    @organisation_id = organisation_id +    copy_set +  end + + +  private + +  # Workers +  # ------- + +  # Copy Set: +  def copy_set +    # Force lazy creation of target_set, just in case source_set is _empty_. +    target_set +    copy_controls  +    copy_blocks +  end + +  # Copy Blocks: +  def copy_block source_block +    target_set.compliance_control_blocks.create( +      name: name_of_copy(:compliance_control_blocks, source_block.name), +      condition_attributes: source_block.condition_attributes).tap do | target_block | +        relink_checks_to_block source_block, target_block  +      end +  end +  def copy_blocks +    source_set.compliance_control_blocks.order(:id).each(&method(:copy_block)) +  end +  def relink_checks_to_block source_block, target_block +    source_block +      .compliance_controls +      .order(:id) +      .each do | source_control | +        control_id_map[source_control.id] +          .update(compliance_control_block_id: target_block.id) +      end +  end + +  # Copy Controls: +  def copy_controls +    source_set.compliance_controls.order(:id).each(&method(:copy_control)) +  end +  def copy_control(compliance_control) +    target_set.compliance_controls.create( +      code: compliance_control.code, +      comment: compliance_control.comment, +      control_attributes: compliance_control.control_attributes, +      criticity: compliance_control.criticity, +      name: name_of_copy(:compliance_controls, compliance_control.name), +      origin_code: compliance_control.origin_code, +      type: compliance_control.type +    ).tap do | control | +      control_id_map.update compliance_control.id => control +    end +  end + +  def name_of_copy resource, name +    [I18n.t("#{resource}.clone.prefix"), name].join(' ') +  end + +  # Lazy Values +  # ----------- +  def organisation +    @__organisation__ ||= Organisation.find(organisation_id) +  end +  def source_set +    @__source_set__ ||= ComplianceControlSet.find(source_set_id) +  end +  def target_set +    @__target_set__ ||= ComplianceControlSet.create!( +      organisation: organisation, +      name: name_of_copy(:compliance_control_sets, source_set.name) +    ) +  end +  def control_id_map +    # Map: compliance_control_id -> compliance_control (origin_id -> copied object) +    @__control_id_to_check__ ||= Hash.new +  end +  def referential +    @__referential__ ||= Referential.find(referential_id) +  end +end diff --git a/lib/tasks/erd.rake b/lib/tasks/erd.rake index 6b79967de..e2665374e 100644 --- a/lib/tasks/erd.rake +++ b/lib/tasks/erd.rake @@ -7,9 +7,9 @@ namespace :generate do      sh "bundle exec rake erd only='Organisation,StopAreaReferential,StopAreaReferentialSync,StopAreaReferentialSyncMessage,StopAreaReferentialMembership,LineReferential,LineReferentialSync,LineReferentialSyncMessage,LineReferentialMembership' filename='referentiels_externes' title='Référentiels externes'"      sh "bundle exec rake erd only='NetexImport,Import,WorkbenchImport,ImportResource,ImportMessage' filename='import' title='Import'"      sh "bundle exec rake erd only='ComplianceControlSet,ComplianceControlBlock,ComplianceControl,ComplianceCheckSet,ComplianceCheckBlock,ComplianceCheck,ComplianceCheckResource,ComplianceCheckMessage' filename='validation' title='Validation'" +    sh "bundle exec rake erd only='Organisation,Workbench,ReferentialSuite,Referential' filename='merge' title='Merge'"      #sh "bundle exec rake erd only='VehicleJourney,VehicleJourneyExport' filename='export' title='Export'" -    #sh "bundle exec rake erd only='' filename='intégration' title='Integration'" -    #sh "bundle exec rake erd only='' filename='fusion' title='Fusion'" +    #sh "bundle exec rake erd only='' filename='integration' title='Integration'"      #sh "bundle exec rake erd only='' filename='publication' title='Publication'"    end diff --git a/spec/controllers/compliance_controls_controller_spec.rb b/spec/controllers/compliance_controls_controller_spec.rb index 34b27530d..61e94025d 100644 --- a/spec/controllers/compliance_controls_controller_spec.rb +++ b/spec/controllers/compliance_controls_controller_spec.rb @@ -44,7 +44,7 @@ RSpec.describe ComplianceControlsController, type: :controller do    describe 'POST #update' do      it 'should be successful' do        post :update, compliance_control_set_id: compliance_control_set.id, id: compliance_control.id, compliance_control: compliance_control.as_json.merge(type: 'GenericAttributeControl::MinMax') -      expect(response).to redirect_to compliance_control_set_compliance_control_path(compliance_control_set, compliance_control) +      expect(response).to redirect_to compliance_control_set_path(compliance_control_set)      end    end diff --git a/spec/factories/imports.rb b/spec/factories/imports.rb index 2c53106c3..e07447b60 100644 --- a/spec/factories/imports.rb +++ b/spec/factories/imports.rb @@ -5,6 +5,23 @@ FactoryGirl.define do      current_step_progress 1.5      association :workbench      association :referential +    file {File.open(File.join(Rails.root, 'spec', 'fixtures', 'OFFRE_TRANSDEV_2017030112251.zip'))} +    status :new +    started_at nil +    ended_at nil +    creator 'rspec' + +    after(:build) do |import| +      import.class.skip_callback(:create, :before, :initialize_fields) +    end +  end + +  factory :bad_import do +    sequence(:name) { |n| "Import #{n}" } +    current_step_id "MyString" +    current_step_progress 1.5 +    association :workbench +    association :referential      file {File.open(File.join(Rails.root, 'spec', 'fixtures', 'terminated_job.json'))}      status :new      started_at nil diff --git a/spec/factories/netex_imports.rb b/spec/factories/netex_imports.rb index 057e47730..9e9d836e4 100644 --- a/spec/factories/netex_imports.rb +++ b/spec/factories/netex_imports.rb @@ -1,5 +1,5 @@  FactoryGirl.define do    factory :netex_import, class: NetexImport, parent: :import do -    file { File.open(Rails.root.join('spec', 'fixtures', 'terminated_job.json')) } +    file { File.open(Rails.root.join('spec', 'fixtures', 'OFFRE_TRANSDEV_2017030112251.zip')) }    end  end diff --git a/spec/factories/workbench_imports.rb b/spec/factories/workbench_imports.rb index 5cdcfd15f..466bfe688 100644 --- a/spec/factories/workbench_imports.rb +++ b/spec/factories/workbench_imports.rb @@ -1,5 +1,5 @@  FactoryGirl.define do    factory :workbench_import, class: WorkbenchImport, parent: :import do -    file { File.open(Rails.root.join('spec', 'fixtures', 'terminated_job.json')) } +    file { File.open(Rails.root.join('spec', 'fixtures', 'OFFRE_TRANSDEV_2017030112251.zip')) }    end  end diff --git a/spec/fixtures/OFFRE_WITH_EXTRA.zip b/spec/fixtures/OFFRE_WITH_EXTRA.zipBinary files differ new file mode 100644 index 000000000..97ea3f513 --- /dev/null +++ b/spec/fixtures/OFFRE_WITH_EXTRA.zip diff --git a/spec/lib/compliance_control_set_cloner_spec.rb b/spec/lib/compliance_control_set_cloner_spec.rb new file mode 100644 index 000000000..4305ec70b --- /dev/null +++ b/spec/lib/compliance_control_set_cloner_spec.rb @@ -0,0 +1,144 @@ +RSpec.describe ComplianceControlSetCloner do + +  subject{ described_class.new } + +  let( :new_organisation ){ create :organisation } + +  let( :source_set ){ create :compliance_control_set } +  let( :set_prefix ){ I18n.t('compliance_control_sets.clone.prefix') } +  let( :block_prefix ){ I18n.t('compliance_control_blocks.clone.prefix') } +  let( :control_prefix ){ I18n.t('compliance_controls.clone.prefix') } + + +  context 'Copying empty set' do + +    context 'correct organisation' do + +      # +      # +      # +      # +      # +      # +      # +      #                                                 +-------------------+ +      #          +---------------------+----------------| Control (direct0) | +      #          |                     |                +-------------------+ +      #          |                     | +      #          |                     |                +-------------------+ +      #          +---------------------)------------+---| Control (direct1) | +      #          |                     |            |   +-------------------+ +      #          |                     |            | +      #          |                     |            |   +-------------------+ +      #          +---------------------)------------)---| Control (direct2) | +      #          |                     |            |   +-------------------+ +      #          |                     |            | +      #          |                     |            | +      #          |                     |            | +      #          v                     v            | +      #   +------------+          +--------------+  |  +---------------------+ +      #   | ControlSet |<----+----| ControlBlock |<-)--| Control (indirect0) | +      #   +------------+     |    +--------------+  |  +---------------------+ +      #                      |                      | +      #                      |    +--------------+<-+  +---------------------+ +      #                      |<---| ControlBlock |<----| Control (indirect1) | +      #                      |    +--------------+     +---------------------+ +      #                      | +      #                      |    +--------------+     +---------------------+ +      #                      +----| ControlBlock |<----| Control (indirect2) | +      #                           +--------------+     +---------------------+ + +      context 'Directed Acyclic Graph is copied correctly' do +        let(:source_blox){ +          3.times.map{ |_| create :compliance_control_block, compliance_control_set: source_set } +        } +        let(:direct_ccs){ +          3.times.map{ |n| create :generic_attribute_control_min_max, compliance_control_set: source_set, name: "direct #{n.succ}", code: "direct-#{n.succ}" } +        } +        # Needed to check we do not dulicate a node (compliance_control) twice +        let(:indirect_ccs){ +          # Create 1 child for each block and also associate first of the direct ccs to the first block +          #                                                  seconf of the direct css to the second block +          source_blox.take(2).zip(direct_ccs.take(2)).each do | source_block, cc | +            cc.update compliance_control_block_id: source_block.id +          end +          source_blox.each_with_index.map{ | source_block, n | +            create(:generic_attribute_control_min_max, compliance_control_set: source_set, compliance_control_block: source_block, name: "indirect #{n.succ}", code: "indirect-#{n.succ}") +          } +        } +        let( :sources ){ source_set.compliance_controls.order(:id) } + +        let( :target_set ){ ComplianceControlSet.last } +        let( :target_blox ){ ComplianceControlBlock.last 3 } +        let( :targets ){ target_set.compliance_controls.order(:id) } + +        before do +          direct_ccs +          indirect_ccs +        end +        it 'correctly creates a set for a complete DAG' do +          # Slowness of tests constrains us to create a minimum of objects in the DB, +          # hence only one example :( +          # +          #  Execute copy and keep count +          counts = object_counts +          subject.copy(source_set.id, new_organisation.id) +          delta  = count_diff counts, object_counts + +          # Check correctly copied set +          expect(target_set.organisation).to eq(new_organisation) +          expect(target_set.name).to eq( [set_prefix, source_set.name].join(' ') ) + +          # Check correctly copied controls +          targets.zip(sources).each do | target, source | +            expect( target.code ).to eq(source.code ) +            expect( target.comment ).to eq(source.comment ) +            expect( target.compliance_control_set ).to eq( target_set ) +            expect( target.control_attributes ).to eq(source.control_attributes) +            expect( target.criticity ).to eq(source.criticity ) +            expect( target.name ).to eq([control_prefix, source.name].join(' ')) +            expect( target.origin_code ).to eq(source.origin_code ) +            expect( target.type ).to eq(source.type) +          end +          # Check correctly copied blocks +          target_blox.zip(source_blox).each do | target_block, source_block | +            expect( target_block.compliance_control_set ).to eq(target_set) +            expect( target_block.name ).to eq( [block_prefix, source_block.name].join(' ') ) +            expect( target_block.condition_attributes ).to eq( source_block.condition_attributes ) +          end + +          # Check correct block associations +          # See diagram above to understand the meaning of this: +          #   - The first two controls have been assigned to the first two blocks accordingly +          #   - The third has no block +          #   - The last three controls have been created from the three blocks in order +          expected_block_ids = target_blox.take(2).map(&:id) + [ nil ] + target_blox.map(&:id) +          expect( targets.pluck(:compliance_control_block_id) ).to eq( expected_block_ids ) + +          # Check overall counts (no additional creations) +          expect( delta ).to eq(counts) +        end +      end + +    end + +    def object_counts +      { +        source_set_count: ComplianceControlSet.count, +        cc_block_count: ComplianceControlBlock.count, +        cc_count: ComplianceControl.count, +        cck_set_count: ComplianceCheckSet.count, +        cck_block_count: ComplianceCheckBlock.count, +        cck_count: ComplianceCheck.count +      } +    end + +    def count_diff count1, count2 +      count1.inject({}){ |h, (k,v)| +        h.merge( k => count2[k] - v ) +      } +    end + +  end + +end diff --git a/spec/models/compliance_control_validations/genric_attribute_validation/min_max_validation_spec.rb b/spec/models/compliance_control_validations/genric_attribute_validation/min_max_validation_spec.rb index 276d09a92..4d30d61e3 100644 --- a/spec/models/compliance_control_validations/genric_attribute_validation/min_max_validation_spec.rb +++ b/spec/models/compliance_control_validations/genric_attribute_validation/min_max_validation_spec.rb @@ -1,8 +1,8 @@ -# RSpec.describe GenericAttributeControl::MinMax do +RSpec.describe GenericAttributeControl::MinMax do -#   let( :factory ){ :generic_attribute_control_min_max } -#   subject{ build factory } +  let( :factory ){ :generic_attribute_control_min_max } +  subject{ build factory } -#   it_behaves_like 'has min_max_values' +  it_behaves_like 'has min_max_values' -# end +end diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb index cd5a30982..c06d05dab 100644 --- a/spec/models/import_spec.rb +++ b/spec/models/import_spec.rb @@ -10,6 +10,9 @@ RSpec.describe Import, type: :model do    it { should validate_presence_of(:workbench) }    it { should validate_presence_of(:creator) } +  it { should allow_value('file.zip').for(:file).with_message(I18n.t('activerecord.errors.models.imports.wrong_file_extension')) } +  it { should_not allow_values('file.json', 'file.png', 'file.pdf').for(:file) } +    let(:workbench_import) { build_stubbed(:workbench_import) }    let(:workbench_import_with_completed_steps) do      workbench_import = build_stubbed( diff --git a/spec/services/zip_service/regression_4273_spec.rb b/spec/services/zip_service/regression_4273_spec.rb deleted file mode 100644 index 4fe0f6539..000000000 --- a/spec/services/zip_service/regression_4273_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -RSpec.describe ZipService do -  describe 'Regression Issue # 4273 https://projects.af83.io/issues/4273' do -    let( :zip_service ){ described_class } -    let( :unzipper ){ zip_service.new(zip_data) } -    let( :zip_data ){ File.read zip_file } - -    context 'real test data' do -      let( :subdir_names ){ %w<OFFRE_TRANSDEV_20170301122517 OFFRE_TRANSDEV_20170301122519>  } -      let( :expected_chksums ){ -        checksum_trees( subdir_names.map{ |sn| subdir_file(sn, prefix: 'source_') } ) -      } - -      let( :zip_file ){ fixtures_path 'OFFRE_TRANSDEV_2017030112251.zip' } -      # -      # Remove potential test artefacts -      before do -        subdir_names.each do | subdir_name | -          File.unlink( subdir_file subdir_name, suffix: '.zip' ) rescue nil -          Dir.unlink( subdir_file subdir_name ) rescue nil -        end -      end - -      it "yields the correct content" do -        subdir_contents = {} -        # Write ZipService Streams to files and inflate them to file system -        unzipper.subdirs.each do | subdir | -          File.open(subdir_file( subdir.name, suffix: '.zip' ), 'wb'){ |f| f.write subdir.stream.string } -          unzip_subdir subdir -        end -        # Represent the inflated file_system as a checksum tree -        actual_checksums =  -          checksum_trees( subdir_names.map{ |sn| subdir_file(sn, prefix: 'target/') } ) -        expect( actual_checksums ).to eq( expected_chksums ) -      end - -    end - -  end - -  def checksum_trees *dirs -    dirs.flatten.inject({},&method(:checksum_tree)) -  end -  def checksum_tree repr, dir -    Dir.glob("#{dir}/**/*").each do |file| -      if !File.directory?(file) -        repr.merge!( File.basename(file) => %x{cksum #{file}}.split.first ){ |_, ov, nv| Array(ov) << nv } -      end -    end -    repr -  end - -  def subdir_file( subdir, prefix: 'target_', suffix: '' ) -    fixtures_path("#{prefix}#{subdir}#{suffix}") -  end - -  def unzip_subdir subdir -    %x{unzip -oqq #{subdir_file subdir.name, suffix: '.zip'} -d #{fixture_path}/target} -  end -end diff --git a/spec/services/zip_service_spec.rb b/spec/services/zip_service_spec.rb new file mode 100644 index 000000000..98cb9026d --- /dev/null +++ b/spec/services/zip_service_spec.rb @@ -0,0 +1,68 @@ +RSpec.describe ZipService do + +  let( :zip_service ){ described_class } +  let( :unzipper ){ zip_service.new(zip_data) } +  let( :zip_data ){ File.read zip_file } + + +  context 'correct test data' do +    before do +      subdir_names.each do | subdir_name | +        File.unlink( subdir_file subdir_name, suffix: '.zip' ) rescue nil +        Dir.unlink( subdir_file subdir_name ) rescue nil +      end +    end +    let( :subdir_names ){ %w<OFFRE_TRANSDEV_20170301122517 OFFRE_TRANSDEV_20170301122519>  } +    let( :expected_chksums ){ +      checksum_trees( subdir_names.map{ |sn| subdir_file(sn, prefix: 'source_') } ) +    } + +    let( :zip_file ){ fixtures_path 'OFFRE_TRANSDEV_2017030112251.zip' } +    # +    # Remove potential test artefacts + +    it 'yields the correct content' do +      # Write ZipService Streams to files and inflate them to file system +      unzipper.subdirs.each do | subdir | +        expect( subdir.spurious ).to be_empty +        File.open(subdir_file( subdir.name, suffix: '.zip' ), 'wb'){ |f| f.write subdir.stream.string } +        unzip_subdir subdir +      end +      # Represent the inflated file_system as a checksum tree +      actual_checksums =  +        checksum_trees( subdir_names.map{ |sn| subdir_file(sn, prefix: 'target/') } ) +      expect( actual_checksums ).to eq( expected_chksums ) +    end + +  end + +  context 'test data with spurious directories' do  +    let( :zip_file ){ fixtures_path 'OFFRE_WITH_EXTRA.zip' } + +    it 'returns the extra dir in the spurious field of the entry' do +      expect( unzipper.subdirs.first.spurious ).to eq(%w{EXTRA}) +    end +  end + + +  def checksum_trees *dirs +    dirs.flatten.inject({},&method(:checksum_tree)) +  end +  def checksum_tree repr, dir +    Dir.glob("#{dir}/**/*").each do |file| +      if !File.directory?(file) +        repr.merge!( File.basename(file) => %x{cksum #{file}}.split.first ){ |_, ov, nv| Array(ov) << nv } +      end +    end +    repr +  end + +  def subdir_file( subdir, prefix: 'target_', suffix: '' ) +    fixtures_path("#{prefix}#{subdir}#{suffix}") +  end + +  def unzip_subdir subdir +    %x{unzip -oqq #{subdir_file subdir.name, suffix: '.zip'} -d #{fixture_path}/target} +  end +end + diff --git a/spec/support/random.rb b/spec/support/random.rb index 59e1a1475..0ebc2ee5e 100644 --- a/spec/support/random.rb +++ b/spec/support/random.rb @@ -22,6 +22,12 @@ module Support      def random_string        SecureRandom.urlsafe_base64      end + +    def very_random(veryness=3, joiner: '-') +      raise ArgumentError, 'not very random' unless veryness > 1 +      veryness.times.map{ SecureRandom.uuid }.join(joiner) +    end +    end  end diff --git a/spec/workers/clean_up_worker_spec.rb b/spec/workers/clean_up_worker_spec.rb index e85768fa3..fd767db00 100644 --- a/spec/workers/clean_up_worker_spec.rb +++ b/spec/workers/clean_up_worker_spec.rb @@ -1,4 +1,3 @@ -require 'rails_helper'  RSpec.describe CleanUpWorker, type: :worker do      pending "add some examples to (or delete) #{__FILE__}"  end diff --git a/spec/workers/compliance_control_set_cloning_worker_spec.rb b/spec/workers/compliance_control_set_cloning_worker_spec.rb new file mode 100644 index 000000000..3a2332f62 --- /dev/null +++ b/spec/workers/compliance_control_set_cloning_worker_spec.rb @@ -0,0 +1,15 @@ +RSpec.describe ComplianceControlSetCloningWorker do + + +  it 'is a worker' do +    expect( described_class.new ).to be_a(Sidekiq::Worker) +  end + +  it 'delegates perform to the correct lib call' do +    id = double('id') +    organisation_id = double('organisation_id') +    expect_any_instance_of(ComplianceControlSetCloner).to receive(:copy).with(id, organisation_id)  +    described_class.new.perform(id, organisation_id) +  end +   +end diff --git a/spec/workers/workbench_import/workbench_import_with_corrupt_zip_spec.rb b/spec/workers/workbench_import/workbench_import_with_corrupt_zip_spec.rb new file mode 100644 index 000000000..5e34b208a --- /dev/null +++ b/spec/workers/workbench_import/workbench_import_with_corrupt_zip_spec.rb @@ -0,0 +1,48 @@ +RSpec.describe WorkbenchImportWorker do + + +  shared_examples_for 'corrupt zipfile data' do +    subject { described_class.new } +    let( :workbench_import ){ create :workbench_import, status: :pending } + +    before do +      # Let us make sure that the name Enterprise will never be forgotten by history, +      # ahem, I meant, that nothing is uploaded, by forbidding any message to be sent +      # to HTTPService +      expect_it.to receive(:download).and_return(downloaded) +    end + +    it 'does not upload' do +      stub_const 'HTTPService', double('HTTPService') +      subject.perform(workbench_import.id) +    end + +    it 'does create a message' do +      expect{ subject.perform(workbench_import.id) }.to change{ workbench_import.messages.count }.by(1) + +      message = workbench_import.messages.last +      expect( message.criticity ).to eq('error') +      expect( message.message_key ).to eq('corrupt_zip_file') +      expect( message.message_attributes ).to eq( 'import_name' => workbench_import.name ) +    end + +    it 'does not change current step' do +      expect{ subject.perform(workbench_import.id) }.not_to change{ workbench_import.current_step } +    end + +    it "sets the workbench_import.status to failed" do +      subject.perform(workbench_import.id) +      expect( workbench_import.reload.status ).to eq('failed') +    end +  end + +  context 'empty zip file' do  +    let( :downloaded ){ '' } +    it_should_behave_like 'corrupt zipfile data' +  end + +  context 'corrupt data' do  +    let( :downloaded ){ very_random } +    it_should_behave_like 'corrupt zipfile data' +  end +end diff --git a/spec/workers/workbench_import_worker_spec.rb b/spec/workers/workbench_import/workbench_import_worker_spec.rb index a349b3433..9f860a6b3 100644 --- a/spec/workers/workbench_import_worker_spec.rb +++ b/spec/workers/workbench_import/workbench_import_worker_spec.rb @@ -5,7 +5,7 @@ RSpec.describe WorkbenchImportWorker, type: [:worker, :request] do    let( :workbench ){ import.workbench }    let( :referential ){ import.referential } -  let( :api_key ){ build_stubbed :api_key, referential: referential, token: "#{referential.id}-#{SecureRandom.hex}" } +  let( :api_key ){ build_stubbed :api_key, referential: referential, token: "#{referential.id}-#{random_hex}" }    # http://www.example.com/workbenches/:workbench_id/imports/:id/download    let( :host ){ Rails.configuration.rails_host } @@ -13,16 +13,17 @@ RSpec.describe WorkbenchImportWorker, type: [:worker, :request] do    let( :downloaded_zip ){ double("downloaded zip") }    let( :download_zip_response ){ OpenStruct.new( body: downloaded_zip ) } -  let( :download_token ){ SecureRandom.urlsafe_base64 } - +  let( :download_token ){ random_string }    let( :upload_path ) { api_v1_netex_imports_path(format: :json) } +  let( :spurious ){ [[], [], []] }    let( :subdirs ) do      entry_count.times.map do |i|        ZipService::Subdir.new(          "subdir #{i}", -        double("subdir #{i}", rewind: 0, read: '') +        double("subdir #{i}", rewind: 0, read: ''), +        spurious[i]        )      end    end @@ -104,8 +105,44 @@ RSpec.describe WorkbenchImportWorker, type: [:worker, :request] do        expect( import ).to receive(:update).with(current_step: 3, status: 'failed')        expect { worker.perform import.id }.to raise_error(StopIteration) +    end +  end + +  context 'multireferential zipfile with spurious directories' do  +    let( :entry_count ){ 2 } +    let( :spurious1 ){ [random_string] } +    let( :spurious2 ){ [random_string, random_string] } +    let( :spurious ){ [spurious1, spurious2] } +    let( :messages ){ double('messages') } +    let( :message_attributes ){{criticity: :warning, message_key: 'inconsistent_zip_file'}} +    let( :message1_attributes ){ message_attributes.merge(message_attributes: {'import_name' => import.name, 'spurious_dirs' => spurious1.inspect}) } +    let( :message2_attributes ){ message_attributes.merge(message_attributes: {'import_name' => import.name, 'spurious_dirs' => spurious2.inspect}) } + +    before do +      allow(import).to receive(:messages).and_return(messages) +    end + +    it 'downloads a zip file, cuts it, and uploads all pieces and adds messages' do + +      expect(HTTPService).to receive(:get_resource) +        .with(host: host, path: path, params: {token: download_token}) +        .and_return( download_zip_response ) + +      subdirs.each do |subdir| +        mock_post subdir, post_response_ok +      end + +      expect( import ).to receive(:update).with(total_steps: 2) +      expect( import ).to receive(:update).with(current_step: 1) +      expect( messages ).to receive(:create).with(message1_attributes) +      expect( import ).to receive(:update).with(current_step: 2) +      expect( messages ).to receive(:create).with(message2_attributes) +      expect( import ).to receive(:update).with(ended_at: Time.now) + +      worker.perform import.id      end +        end    def mock_post subdir, response | 
