diff --git a/.gitignore b/.gitignore index 9a9dd9ca4..4af928a07 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ yarn-debug.log* /public/packs /public/packs-test /node_modules +vendor/* /yarn-error.log yarn-debug.log* .yarn-integrity diff --git a/Gemfile b/Gemfile index be3d60fb6..7ffa37c1c 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,7 @@ gem 'lograge' gem 'logstash-event' gem 'mailjet' gem 'omniauth-github' +gem 'omniauth-rails_csrf_protection', '~> 0.1' gem 'openid_connect' gem 'openstack' gem 'pg' @@ -66,6 +67,7 @@ gem 'turbolinks' # Turbolinks makes following links in your web application fast gem 'typhoeus' gem 'warden' gem 'webpacker' +gem 'zipline' gem 'zxcvbn-ruby', require: 'zxcvbn' group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 4e80820c2..b925daa7a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -170,6 +170,7 @@ GEM crass (1.0.4) css_parser (1.6.0) addressable + curb (0.9.10) daemons (1.3.1) database_cleaner (1.7.0) datetime_picker_rails (0.0.7) @@ -386,6 +387,9 @@ GEM omniauth-oauth2 (1.6.0) oauth2 (~> 1.1) omniauth (~> 1.9) + omniauth-rails_csrf_protection (0.1.2) + actionpack (>= 4.2) + omniauth (>= 1.3.1) open4 (1.3.4) openid_connect (1.1.6) activemodel @@ -671,6 +675,11 @@ GEM nokogiri (~> 1.8) xray-rails (0.3.1) rails (>= 3.1.0) + zip_tricks (4.7.4) + zipline (1.1.0) + curb (>= 0.8.0, < 0.10) + rails (>= 3.2.1, < 6.1) + zip_tricks (>= 4.2.1, <= 5.0.0) zxcvbn-ruby (0.1.2) PLATFORMS @@ -731,6 +740,7 @@ DEPENDENCIES mailjet mina! omniauth-github + omniauth-rails_csrf_protection (~> 0.1) openid_connect openstack pg @@ -775,6 +785,7 @@ DEPENDENCIES webmock webpacker xray-rails + zipline zxcvbn-ruby BUNDLED WITH diff --git a/app/controllers/gestionnaires/dossiers_controller.rb b/app/controllers/gestionnaires/dossiers_controller.rb index d58fed8fb..adbc9e237 100644 --- a/app/controllers/gestionnaires/dossiers_controller.rb +++ b/app/controllers/gestionnaires/dossiers_controller.rb @@ -5,6 +5,9 @@ module Gestionnaires include CreateAvisConcern include DossierHelper + include ActionController::Streaming + include Zipline + after_action :mark_demande_as_read, only: :show after_action :mark_messagerie_as_read, only: [:messagerie, :create_commentaire] after_action :mark_avis_as_read, only: [:avis, :create_avis] @@ -172,6 +175,14 @@ module Gestionnaires render layout: "print" end + def telecharger_pjs + return head(:forbidden) if !Flipflop.download_as_zip_enabled? || !dossier.attachments_downloadable? + + files = ActiveStorage::DownloadableFile.create_list_from_dossier(dossier) + + zipline(files, "dossier-#{dossier.id}.zip") + end + private def dossier diff --git a/app/controllers/manager/dossiers_controller.rb b/app/controllers/manager/dossiers_controller.rb index 31867443f..35b7c785c 100644 --- a/app/controllers/manager/dossiers_controller.rb +++ b/app/controllers/manager/dossiers_controller.rb @@ -22,10 +22,10 @@ module Manager def hide dossier = Dossier.find(params[:id]) - dossier.hide!(current_administration) + dossier.delete_and_keep_track(current_administration) logger.info("Le dossier #{dossier.id} est supprimé par #{current_administration.email}") - flash[:notice] = "Le dossier #{dossier.id} est supprimé" + flash[:notice] = "Le dossier #{dossier.id} a été supprimé." redirect_to manager_dossier_path(dossier) end diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 3e13bbc46..4479e5a18 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -193,7 +193,7 @@ module Users dossier = current_user.dossiers.includes(:user, procedure: :administrateurs).find(params[:id]) if dossier.can_be_deleted_by_user? - dossier.delete_and_keep_track + dossier.delete_and_keep_track(current_user) flash.notice = 'Votre dossier a bien été supprimé.' redirect_to dossiers_path else diff --git a/app/lib/active_storage/downloadable_file.rb b/app/lib/active_storage/downloadable_file.rb new file mode 100644 index 000000000..3ef733e8d --- /dev/null +++ b/app/lib/active_storage/downloadable_file.rb @@ -0,0 +1,29 @@ +class ActiveStorage::DownloadableFile + def initialize(attached) + if using_local_backend? + @url = 'file://' + ActiveStorage::Blob.service.path_for(attached.key) + else + @url = attached.service_url + end + end + + def url + @url + end + + def self.create_list_from_dossier(dossier) + pjs = PiecesJustificativesService.liste_pieces_justificatives(dossier) + pjs.map do |pj| + [ + ActiveStorage::DownloadableFile.new(pj.piece_justificative_file), + pj.piece_justificative_file.filename.to_s + ] + end + end + + private + + def using_local_backend? + [:local, :local_test, :test].include?(Rails.application.config.active_storage.service) + end +end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 81c426824..a0251397b 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -317,7 +317,7 @@ class Dossier < ApplicationRecord end end - def delete_and_keep_track + def delete_and_keep_track(author) deleted_dossier = DeletedDossier.create_from_dossier(self) update(hidden_at: deleted_dossier.deleted_at) @@ -328,6 +328,8 @@ class Dossier < ApplicationRecord end end DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later + + log_dossier_operation(author, :supprimer, self) end def after_passer_en_instruction(gestionnaire) @@ -409,14 +411,6 @@ class Dossier < ApplicationRecord log_dossier_operation(gestionnaire, :classer_sans_suite, self) end - def hide!(administration) - update(hidden_at: Time.zone.now) - - deleted_dossier = DeletedDossier.create_from_dossier(self) - DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later - log_dossier_operation(administration, :supprimer, self) - end - def check_mandatory_champs (champs + champs.select(&:repetition?).flat_map(&:champs)) .select(&:mandatory_and_blank?) @@ -466,6 +460,10 @@ class Dossier < ApplicationRecord end end + def attachments_downloadable? + !PiecesJustificativesService.liste_pieces_justificatives(self).empty? && PiecesJustificativesService.pieces_justificatives_total_size(self) < 50.megabytes + end + private def log_dossier_operation(author, operation, subject = nil) diff --git a/app/services/carrierwave_active_storage_migration_service.rb b/app/services/carrierwave_active_storage_migration_service.rb index 1183cf8f9..d5a4c59a5 100644 --- a/app/services/carrierwave_active_storage_migration_service.rb +++ b/app/services/carrierwave_active_storage_migration_service.rb @@ -154,10 +154,20 @@ class CarrierwaveActiveStorageMigrationService attachment end - def fix_content_type(blob) + def fix_content_type(blob, retry_delay: 5) + retries ||= 0 # In OpenStack, ActiveStorage cannot inject the MIME type on the fly during direct # download. Instead, the MIME type needs to be stored statically on the file object # in OpenStack. This is what this call does. blob.service.change_content_type(blob.key, blob.content_type) + rescue + # When we quickly create a new attachment, and then change its content type, + # the Object Storage may not be synchronized yet. It this cas, it will return a + # "409 Conflict" error. + # + # Wait for a while, then try again twice (before giving up). + sleep(retry_delay) + retry if (retries += 1) < 3 + raise end end diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index 411e11124..da7d53325 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -70,6 +70,17 @@ class PiecesJustificativesService end end + def self.liste_pieces_justificatives(dossier) + dossier.champs + .select { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) } + .filter { |pj| pj.piece_justificative_file.attached? } + end + + def self.pieces_justificatives_total_size(dossier) + liste_pieces_justificatives(dossier) + .sum { |pj| pj.piece_justificative_file.byte_size } + end + def self.serialize_types_de_champ_as_type_pj(procedure) tdcs = procedure.types_de_champ.select { |type_champ| type_champ.old_pj.present? } tdcs.map.with_index do |type_champ, order_place| diff --git a/app/views/administrations/sessions/new.html.haml b/app/views/administrations/sessions/new.html.haml index 8ed5fb4d9..3176e8927 100644 --- a/app/views/administrations/sessions/new.html.haml +++ b/app/views/administrations/sessions/new.html.haml @@ -1,6 +1,6 @@ .super-admin.flex.justify-center %div %h2 Espace Admin - = link_to administration_github_omniauth_authorize_path, class: "button large" do + = link_to administration_github_omniauth_authorize_path, method: :post, class: "button large" do %span.icon.lock Connexion avec GitHub diff --git a/app/views/gestionnaires/dossiers/_header.html.haml b/app/views/gestionnaires/dossiers/_header.html.haml index 186d4c4d6..61afc1361 100644 --- a/app/views/gestionnaires/dossiers/_header.html.haml +++ b/app/views/gestionnaires/dossiers/_header.html.haml @@ -18,6 +18,14 @@ = link_to "Tout le dossier", print_gestionnaire_dossier_path(dossier.procedure, dossier), target: "_blank", rel: "noopener", class: "menu-item menu-link" %li = link_to "Uniquement cet onglet", "#", onclick: "window.print()", class: "menu-item menu-link" + - if Flipflop.download_as_zip_enabled? && dossier.attachments_downloadable? + %span.dropdown.print-menu-opener + %button.button.dropdown-button.icon-only + %span.icon.attachment + %ul.print-menu.dropdown-content + %li + = link_to "Télécharger toutes les pièces jointes", telecharger_pjs_gestionnaire_dossier_path(dossier.procedure, dossier), target: "_blank", rel: "noopener", class: "menu-item menu-link" + = render partial: "gestionnaires/procedures/dossier_actions", locals: { procedure: dossier.procedure, dossier: dossier, dossier_is_followed: current_gestionnaire&.follow?(dossier) } %span.state-button diff --git a/app/views/invites/_form.html.haml b/app/views/invites/_form.html.haml index 4268d522d..a35dbe5f9 100644 --- a/app/views/invites/_form.html.haml +++ b/app/views/invites/_form.html.haml @@ -6,7 +6,7 @@ %li= invite.email %p Ces personnes peuvent modifier ce dossier. - if dossier.brouillon? - %p Une fois le dossier complet, vous devez le soumettre vous-même. + %p Une fois le dossier complet, vous devez le déposer vous-même. - else %p Vous pouvez inviter quelqu’un à remplir ce dossier avec vous. diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml index 97f0b494d..ed34765e0 100644 --- a/app/views/shared/dossiers/_edit.html.haml +++ b/app/views/shared/dossiers/_edit.html.haml @@ -80,7 +80,7 @@ data: { 'disable-with': "Envoi en cours…" } - if dossier.can_transition_to_en_construction? - = f.button 'Soumettre le dossier', + = f.button 'Déposer le dossier', class: 'button send primary', disabled: !current_user.owns?(dossier), data: { 'disable-with': "Envoi en cours…" } @@ -92,6 +92,6 @@ - if dossier.brouillon? && !current_user.owns?(dossier) .send-notice.invite-cannot-submit - En tant qu’invité, vous pouvez remplir ce formulaire – mais le titulaire du dossier doit le soumettre lui-même. + En tant qu’invité, vous pouvez remplir ce formulaire – mais le titulaire du dossier doit le déposer lui-même. = render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier } diff --git a/app/views/users/dossiers/show/_estimated_delay.html.haml b/app/views/users/dossiers/show/_estimated_delay.html.haml index 2bea129cf..eabc51bb3 100644 --- a/app/views/users/dossiers/show/_estimated_delay.html.haml +++ b/app/views/users/dossiers/show/_estimated_delay.html.haml @@ -7,3 +7,5 @@ - if procedure.usual_traitement_time && show_time_means %p Habituellement, les dossiers de cette démarche sont traités dans un délai de #{distance_of_time_in_words(procedure.usual_traitement_time)}. + %p + Cette estimation est calculée automatiquement à partir des délais d’instruction constatés précédemment. Le délai réel peut être différent, en fonction du type de démarche (par exemple pour un appel à projet avec date de décision fixe). diff --git a/app/views/users/dossiers/show/_status_overview.html.haml b/app/views/users/dossiers/show/_status_overview.html.haml index b5504237f..945b18e63 100644 --- a/app/views/users/dossiers/show/_status_overview.html.haml +++ b/app/views/users/dossiers/show/_status_overview.html.haml @@ -1,5 +1,3 @@ - - .status-overview - if !dossier.termine? %ul.status-timeline @@ -16,28 +14,37 @@ .status-explanation - if dossier.brouillon? .brouillon - %p Vous pouvez remplir votre dossier tranquillement : il n’est pas encore visible par l’administration. - %p Quand vous aurez terminé, soumettez votre dossier pour qu’il soit examiné. + %p Votre dossier n’est pas encore visible par l’administration. + %p Vous pouvez déposer votre dossier afin de le transmettre à l’administration. Une fois soumis, vous pourrez toujours modifier votre dossier. - elsif dossier.en_construction? .en-construction - %p Un instructeur de l’administration est en train de vérifier que votre dossier est bien complet. Si des modifications sont nécessaires, vous recevrez un message avec les modifications à effectuer. %p - Sinon, + Votre dossier est en construction. Cela signifie que = succeed '.' do - %strong votre dossier passera directement en instruction + %strong vous pouvez encore le modifier + Vous ne pourrez plus modifier votre dossier lorsque l’administration le passera « en instruction ». + = render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure } + %p + %strong Vous avez une question ? + Utilisez la messagerie pour + = succeed '.' do + = link_to 'contacter l’administration directement', messagerie_dossier_url(dossier) + - elsif dossier.en_instruction? .en-instruction - %p Votre dossier est complet. Il est en cours d’examen par les instructeurs de l’administration. - %p - Dès que l’administration aura statué sur votre dossier, - %strong - vous recevrez un email - avec le résultat. + %p Votre dossier est en cours d’instruction par l’administration. Vous ne pouvez plus le modifier. + = render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure } + %p + %strong Vous avez une question ? + Utilisez la messagerie pour + = succeed '.' do + = link_to 'contacter l’administration directement', messagerie_dossier_url(dossier) + - elsif dossier.accepte? .accepte %p.decision diff --git a/config/features.rb b/config/features.rb index 1f7816d14..d4bb54e21 100644 --- a/config/features.rb +++ b/config/features.rb @@ -18,6 +18,8 @@ Flipflop.configure do feature :procedure_export_v2_enabled feature :operation_log_serialize_subject + feature :download_as_zip_enabled, + default: false group :development do feature :mini_profiler_enabled, diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 000000000..1c1946a5c --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,4 @@ +# OmniAuth GET requests may be vulnerable to CSRF. +# Ensure that OmniAuth only uses POST requests. +# See https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284 +OmniAuth.config.allowed_request_methods = [:post] diff --git a/config/routes.rb b/config/routes.rb index bb9785886..6abccf8e2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -335,6 +335,7 @@ Rails.application.routes.draw do post 'send-to-instructeurs' => 'dossiers#send_to_instructeurs' post 'avis' => 'dossiers#create_avis' get 'print' => 'dossiers#print' + get 'telecharger_pjs' => 'dossiers#telecharger_pjs' end end end diff --git a/lib/tasks/support.rake b/lib/tasks/support.rake index 9cdb35adb..7d96ac8e2 100644 --- a/lib/tasks/support.rake +++ b/lib/tasks/support.rake @@ -2,19 +2,26 @@ require Rails.root.join("lib", "tasks", "task_helper") namespace :support do desc <<~EOD - Delete the user account for a given USER_EMAIL. + Delete the user account for a given USER_EMAIL on the behalf of ADMIN_EMAIL. Only works if the user has no dossier where the instruction has started. EOD task delete_user_account: :environment do user_email = ENV['USER_EMAIL'] - if user_email.nil? - fail "Must specify a USER_EMAIL" - end - user = User.find_by(email: user_email) + fail "Must specify a USER_EMAIL" if user_email.nil? + + administration_email = ENV['ADMIN_EMAIL'] + fail "Must specify the ADMIN_EMAIL of the operator performing the deletion (yourself)" if administration_email.nil? + + user = User.find_by!(email: user_email) + administration = Administration.find_by!(email: administration_email) + if user.dossiers.state_instruction_commencee.any? fail "Cannot delete this user because instruction has started for some dossiers" end - user.dossiers.each(&:delete_and_keep_track) + + user.dossiers.each do |dossier| + dossier.delete_and_keep_track(administration) + end user.destroy end diff --git a/spec/controllers/gestionnaires/dossiers_controller_spec.rb b/spec/controllers/gestionnaires/dossiers_controller_spec.rb index cbf5b775a..0f327221a 100644 --- a/spec/controllers/gestionnaires/dossiers_controller_spec.rb +++ b/spec/controllers/gestionnaires/dossiers_controller_spec.rb @@ -524,4 +524,24 @@ describe Gestionnaires::DossiersController, type: :controller do it { expect(champ_repetition.champs.first.value).to eq('text') } it { expect(response).to redirect_to(annotations_privees_gestionnaire_dossier_path(dossier.procedure, dossier)) } end + + describe "#telecharger_pjs" do + subject do + get :telecharger_pjs, params: { + procedure_id: procedure.id, + dossier_id: dossier.id + } + end + + context 'when zip download is disabled through flipflop' do + before do + Flipflop::FeatureSet.current.test!.switch!(:download_as_zip_enabled, false) + end + + it 'is forbidden' do + subject + expect(response).to have_http_status(:forbidden) + end + end + end end diff --git a/spec/features/users/brouillon_spec.rb b/spec/features/users/brouillon_spec.rb index a0eef004f..6788c9b41 100644 --- a/spec/features/users/brouillon_spec.rb +++ b/spec/features/users/brouillon_spec.rb @@ -137,14 +137,14 @@ feature 'The user' do expect(page).to have_current_path(brouillon_dossier_path(user_dossier)) # Check an incomplete dossier cannot be submitted when mandatory fields are missing - click_on 'Soumettre le dossier' + click_on 'Déposer le dossier' expect(user_dossier.reload.brouillon?).to be(true) expect(page).to have_current_path(brouillon_dossier_path(user_dossier)) # Check a dossier can be submitted when all mandatory fields are filled fill_in('texte obligatoire', with: 'super texte') - click_on 'Soumettre le dossier' + click_on 'Déposer le dossier' expect(user_dossier.reload.en_construction?).to be(true) expect(champ_value_for('texte obligatoire')).to eq('super texte') expect(page).to have_current_path(merci_dossier_path(user_dossier)) diff --git a/spec/features/users/invite_spec.rb b/spec/features/users/invite_spec.rb index c8706dc7d..e1f8c58b2 100644 --- a/spec/features/users/invite_spec.rb +++ b/spec/features/users/invite_spec.rb @@ -63,7 +63,7 @@ feature 'Invitations' do navigate_to_invited_dossier(invite) expect(page).to have_current_path(brouillon_dossier_path(dossier)) - expect(page).to have_button('Soumettre le dossier', disabled: true) + expect(page).to have_button('Déposer le dossier', disabled: true) expect(page).to have_selector('.invite-cannot-submit') end end diff --git a/spec/lib/active_storage/downloadable_file_spec.rb b/spec/lib/active_storage/downloadable_file_spec.rb new file mode 100644 index 000000000..b802a90c9 --- /dev/null +++ b/spec/lib/active_storage/downloadable_file_spec.rb @@ -0,0 +1,26 @@ +describe ActiveStorage::DownloadableFile do + let(:tpjs) { [tpj_not_mandatory] } + let!(:tpj_not_mandatory) do + TypeDePieceJustificative.create(libelle: 'not mandatory', mandatory: false) + end + let(:procedure) { Procedure.create(types_de_piece_justificative: tpjs) } + let(:dossier) { Dossier.create(procedure: procedure) } + let(:procedure) { Procedure.create(types_de_piece_justificative: tpjs) } + let(:dossier) { Dossier.create(procedure: procedure) } + let(:list) { ActiveStorage::DownloadableFile.create_list_from_dossier(dossier) } + + describe 'create_list_from_dossier' do + context 'when no piece_justificative is present' do + it { expect(list).to match([]) } + end + + context 'when there is a piece_justificative' do + let (:pj) { create(:champ, :piece_justificative, :with_piece_justificative_file) } + before do + dossier.champs = [pj] + end + + it { expect(list.length).to be 1 } + end + end +end diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index d1ac82b16..312e5d9ab 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -99,6 +99,14 @@ describe Dossier do end end + describe '#types_de_piece_justificative' do + subject { dossier.types_de_piece_justificative } + it 'returns list of required piece justificative' do + expect(subject.size).to eq(2) + expect(subject).to include(TypeDePieceJustificative.find(TypeDePieceJustificative.first.id)) + end + end + describe '#retrieve_last_piece_justificative_by_type', vcr: { cassette_name: 'models_dossier_retrieve_last_piece_justificative_by_type' } do let(:types_de_pj_dossier) { dossier.procedure.types_de_piece_justificative } @@ -113,6 +121,20 @@ describe Dossier do end end + describe '#retrieve_all_piece_justificative_by_type' do + let(:types_de_pj_dossier) { dossier.procedure.types_de_piece_justificative } + + subject { dossier.retrieve_all_piece_justificative_by_type types_de_pj_dossier.first } + + before do + create :piece_justificative, :rib, dossier: dossier, type_de_piece_justificative: types_de_pj_dossier.first + end + + it 'returns a list of the piece justificative' do + expect(subject).not_to be_empty + end + end + describe '#build_default_champs' do context 'when dossier is linked to a procedure with type_de_champ_public and private' do let(:dossier) { create(:dossier, user: user) } @@ -610,13 +632,14 @@ describe Dossier do describe "#delete_and_keep_track" do let(:dossier) { create(:dossier) } let(:deleted_dossier) { DeletedDossier.find_by!(dossier_id: dossier.id) } + let(:last_operation) { dossier.dossier_operation_logs.last } before do allow(DossierMailer).to receive(:notify_deletion_to_user).and_return(double(deliver_later: nil)) allow(DossierMailer).to receive(:notify_deletion_to_administration).and_return(double(deliver_later: nil)) end - subject! { dossier.delete_and_keep_track } + subject! { dossier.delete_and_keep_track(dossier.user) } it 'hides the dossier' do expect(dossier.hidden_at).to be_present @@ -633,6 +656,11 @@ describe Dossier do expect(DossierMailer).to have_received(:notify_deletion_to_user).with(deleted_dossier, dossier.user.email) end + it 'records the operation in the log' do + expect(last_operation.operation).to eq("supprimer") + expect(last_operation.automatic_operation?).to be_falsey + end + context 'where gestionnaires are following the dossier' do let(:dossier) { create(:dossier, :en_construction, :followed) } let!(:non_following_gestionnaire) do @@ -968,23 +996,6 @@ describe Dossier do end end - describe '#hide!' do - let(:dossier) { create(:dossier) } - let(:administration) { create(:administration) } - let(:last_operation) { dossier.dossier_operation_logs.last } - - before do - Timecop.freeze - dossier.hide!(administration) - end - - after { Timecop.return } - - it { expect(dossier.hidden_at).to eq(Time.zone.now) } - it { expect(last_operation.operation).to eq('supprimer') } - it { expect(last_operation.automatic_operation?).to be_falsey } - end - describe '#repasser_en_instruction!' do let(:dossier) { create(:dossier, :refuse, :with_attestation) } let!(:gestionnaire) { create(:gestionnaire) } @@ -1008,4 +1019,32 @@ describe Dossier do after { Timecop.return } end + + describe '#attachments_downloadable?' do + let(:dossier) { create(:dossier, user: user) } + # subject { dossier.attachments_downloadable? } + + context "no attachments" do + it { + expect(PiecesJustificativesService).to receive(:liste_pieces_justificatives).and_return([]) + expect(dossier.attachments_downloadable?).to be false + } + end + + context "with a small attachment" do + it { + expect(PiecesJustificativesService).to receive(:liste_pieces_justificatives).and_return([Champ.new]) + expect(PiecesJustificativesService).to receive(:pieces_justificatives_total_size).and_return(4.megabytes) + expect(dossier.attachments_downloadable?).to be true + } + end + + context "with a too large attachment" do + it { + expect(PiecesJustificativesService).to receive(:liste_pieces_justificatives).and_return([Champ.new]) + expect(PiecesJustificativesService).to receive(:pieces_justificatives_total_size).and_return(100.megabytes) + expect(dossier.attachments_downloadable?).to be false + } + end + end end diff --git a/spec/services/carrierwave_active_storage_migration_service_spec.rb b/spec/services/carrierwave_active_storage_migration_service_spec.rb index fb70b0d6f..abf7ff1ca 100644 --- a/spec/services/carrierwave_active_storage_migration_service_spec.rb +++ b/spec/services/carrierwave_active_storage_migration_service_spec.rb @@ -69,4 +69,31 @@ describe CarrierwaveActiveStorageMigrationService do end end end + + describe '.fix_content_type' do + let(:pj) { create(:piece_justificative, :rib, updated_at: Time.zone.local(2019, 01, 01, 12, 00)) } + let(:blob) { service.make_empty_blob(pj.content, pj.updated_at.iso8601, filename: pj.original_filename) } + + context 'when the request is ok' do + it 'succeeds' do + expect(blob.service).to receive(:change_content_type).and_return(true) + expect { service.fix_content_type(blob) }.not_to raise_error + end + end + + context 'when the request fails initially' do + it 'retries the request' do + expect(blob.service).to receive(:change_content_type).and_raise(StandardError).ordered + expect(blob.service).to receive(:change_content_type).and_return(true).ordered + expect { service.fix_content_type(blob, retry_delay: 0.01) }.not_to raise_error + end + end + + context 'when the request fails too many times' do + it 'gives up' do + expect(blob.service).to receive(:change_content_type).and_raise(StandardError).thrice + expect { service.fix_content_type(blob, retry_delay: 0.01) }.to raise_error(StandardError) + end + end + end end diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index 3d8eb6d09..6988e3007 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -20,6 +20,9 @@ describe PiecesJustificativesService do let(:errors) { PiecesJustificativesService.upload!(dossier, user, hash) } let(:tpjs) { [tpj_not_mandatory] } + let(:attachment_list) { PiecesJustificativesService.liste_pieces_justificatives(dossier) } + let(:poids_total) { PiecesJustificativesService.pieces_justificatives_total_size(dossier) } + describe 'self.upload!' do context 'when no params are given' do it { expect(errors).to eq([]) } @@ -81,12 +84,30 @@ describe PiecesJustificativesService do before :each do # we are messing around piece_justificative # because directly doubling carrierwave params seems complicated - piece_justificative_double = double(type_de_piece_justificative: tpj_mandatory) expect(dossier).to receive(:pieces_justificatives).and_return([piece_justificative_double]) end - it { expect(errors).to match([]) } + it { + expect(errors).to match([]) + } + end + end + + describe '#attachment_list' do + context 'when no piece_justificative is present' do + it { expect(attachment_list).to match([]) } + it { expect(poids_total).to be 0 } + end + + context 'when there is a piece_justificative' do + let (:pj) { create(:champ, :piece_justificative, :with_piece_justificative_file) } + before do + dossier.champs = [pj] + end + + it { expect(attachment_list).not_to be_empty } + it { expect(poids_total).to be pj.piece_justificative_file.byte_size } end end diff --git a/yarn.lock b/yarn.lock index 1b73ba830..c3b8f9511 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5114,9 +5114,9 @@ lodash.uniq@^4.5.0: integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@~4.17.10: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + version "4.17.14" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" + integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== loglevel@^1.6.3: version "1.6.3"