demarches-normaliennes/spec/services/procedure_export_service_spec.rb
2024-08-30 16:43:30 +02:00

537 lines
20 KiB
Ruby

# frozen_string_literal: true
require 'csv'
describe ProcedureExportService do
let(:instructeur) { create(:instructeur) }
let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) }
let(:export_template) { nil }
describe 'to_xlsx' do
subject do
service
.to_xlsx
.open { |f| SimpleXlsxReader.open(f.path) }
end
let(:dossiers_sheet) { subject.sheets.first }
let(:etablissements_sheet) { subject.sheets.second }
let(:avis_sheet) { subject.sheets.third }
let(:repetition_sheet) { subject.sheets.fourth }
describe 'sheets' do
let(:procedure) { create(:procedure) }
it 'should have a sheet for each record type' do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis'])
end
end
describe 'Dossiers sheet' do
context 'with all data for individual' do
let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) }
let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) }
# before do
# # change one tdc place to check if the header is ordered
# tdc_first = procedure.active_revision.revision_types_de_champ_public.first
# tdc_last = procedure.active_revision.revision_types_de_champ_public.last
# tdc_first.update(position: tdc_last.position + 1)
# procedure.reload
# end
let(:nominal_headers) do
[
"ID",
"Email",
"FranceConnect ?",
"Civilité",
"Nom",
"Prénom",
"Dépôt pour un tiers",
"Nom du mandataire",
"Prénom du mandataire",
"Archivé",
"État du dossier",
"Dernière mise à jour le",
"Dernière mise à jour du dossier le",
"Déposé le",
"Passé en instruction le",
"Traité le",
"Motivation de la décision",
"Instructeurs",
"textarea",
"date",
"datetime",
"number",
"decimal_number",
"integer_number",
"checkbox",
"civilite",
"email",
"phone",
"address",
"simple_drop_down_list",
"multiple_drop_down_list",
"linked_drop_down_list",
"communes",
"communes (Code INSEE)",
"communes (Département)",
"departements",
"departements (Code)",
"regions",
"regions (Code)",
"pays",
"pays (Code)",
"dossier_link",
"piece_justificative",
"rna",
"carte",
"titre_identite",
"iban",
"siret",
"annuaire_education",
"cnaf",
"dgfip",
"pole_emploi",
"mesri",
"text",
"epci",
"epci (Code)",
"epci (Département)",
"cojo",
"expression_reguliere",
"rnf",
"rnf (Nom)",
"rnf (Adresse)",
"rnf (Code INSEE Ville)",
"rnf (Département)",
"engagement_juridique",
"yes_no"
]
end
it 'should have data' do
expect(dossiers_sheet.headers).to match_array(nominal_headers)
expect(dossiers_sheet.data.size).to eq(1)
expect(etablissements_sheet.data.size).to eq(1)
# SimpleXlsxReader is transforming datetimes in utc... It is only used in test so we just hack around.
offset = dossier.depose_at.utc_offset
depose_at = Time.zone.at(dossiers_sheet.data[0][13] - offset.seconds)
en_instruction_at = Time.zone.at(dossiers_sheet.data[0][14] - offset.seconds)
expect(dossiers_sheet.data.first.size).to eq(nominal_headers.size)
expect(depose_at).to eq(dossier.depose_at.round)
expect(en_instruction_at).to eq(dossier.en_instruction_at.round)
end
end
context 'with a birthdate' do
let(:procedure) { create(:procedure, :published, :for_individual, ask_birthday: true) }
let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:) }
it 'find date de naissance' do
expect(dossiers_sheet.headers).to include('Date de naissance')
expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Date de naissance')]).to be_a(Date)
end
end
context 'with a procedure routee' do
let(:procedure) { create(:procedure, :published, :for_individual) }
let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:) }
before { create(:groupe_instructeur, label: '2', procedure:) }
it 'find groupe instructeur data' do
expect(dossiers_sheet.headers).to include('Groupe instructeur')
expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Groupe instructeur')]).to eq('défaut')
end
end
context 'with a dossier having multiple pjs' do
let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) }
let(:types_de_champ_public) { [{ type: :piece_justificative }] }
let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure:) }
let!(:dossier_2) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure:) }
before do
dossier_2.champs_public
.find { _1.is_a? Champs::PieceJustificativeChamp }
.piece_justificative_file
.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain")
end
it { expect(dossiers_sheet.data.first.size).to eq(19) } # default number of header when procedure has only one champ
end
context 'with procedure chorus' do
before { expect_any_instance_of(Procedure).to receive(:chorusable?).and_return(true) }
let(:procedure) { create(:procedure, :published, :for_individual, :filled_chorus) }
let!(:dossier) { create(:dossier, :en_instruction, procedure: procedure) }
it 'includes chorus headers' do
expect(dossiers_sheet.headers).to include('Domaine Fonctionnel')
expect(dossiers_sheet.headers).to include('Référentiel De Programmation')
expect(dossiers_sheet.headers).to include('Centre De Coût')
end
end
end
describe 'Etablissement sheet' do
let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
let(:types_de_champ_public) { [{ type: :siret, libelle: 'siret' }] }
let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_entreprise, procedure: procedure) }
let(:dossier_etablissement) { etablissements_sheet.data[1] }
let(:champ_etablissement) { etablissements_sheet.data[0] }
let(:nominal_headers) do
[
"ID",
"Email",
"FranceConnect ?",
"Entreprise raison sociale",
"Archivé",
"État du dossier",
"Dernière mise à jour le",
"Dernière mise à jour du dossier le",
"Déposé le",
"Passé en instruction le",
"Traité le",
"Motivation de la décision",
"Instructeurs",
'siret'
]
end
context 'as csv' do
subject do
ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template)
.to_csv
.open { |f| CSV.read(f.path) }
end
let(:nominal_headers) do
[
"ID",
"Email",
"FranceConnect ?",
"Établissement SIRET",
"Établissement siège social",
"Établissement NAF",
"Établissement libellé NAF",
"Établissement Adresse",
"Établissement numero voie",
"Établissement type voie",
"Établissement nom voie",
"Établissement complément adresse",
"Établissement code postal",
"Établissement localité",
"Établissement code INSEE localité",
"Entreprise SIREN",
"Entreprise capital social",
"Entreprise numero TVA intracommunautaire",
"Entreprise forme juridique",
"Entreprise forme juridique code",
"Entreprise nom commercial",
"Entreprise raison sociale",
"Entreprise SIRET siège social",
"Entreprise code effectif entreprise",
"Entreprise date de création",
"Entreprise état administratif",
"Entreprise nom",
"Entreprise prénom",
"Association RNA",
"Association titre",
"Association objet",
"Association date de création",
"Association date de déclaration",
"Association date de publication",
"Archivé",
"État du dossier",
"Dernière mise à jour le",
"Dernière mise à jour du dossier le",
"Déposé le",
"Passé en instruction le",
"Traité le",
"Motivation de la décision",
"Instructeurs",
'siret'
]
end
let(:dossiers_sheet_headers) { subject.first }
it 'should have headers' do
expect(dossiers_sheet_headers).to match_array(nominal_headers)
end
end
it 'should have headers and data' do
expect(dossiers_sheet.headers).to match_array(nominal_headers)
expect(etablissements_sheet.headers).to eq([
"Dossier ID",
"Champ",
"Établissement SIRET",
"Etablissement enseigne",
"Établissement siège social",
"Établissement NAF",
"Établissement libellé NAF",
"Établissement Adresse",
"Établissement numero voie",
"Établissement type voie",
"Établissement nom voie",
"Établissement complément adresse",
"Établissement code postal",
"Établissement localité",
"Établissement code INSEE localité",
"Entreprise SIREN",
"Entreprise capital social",
"Entreprise numero TVA intracommunautaire",
"Entreprise forme juridique",
"Entreprise forme juridique code",
"Entreprise nom commercial",
"Entreprise raison sociale",
"Entreprise SIRET siège social",
"Entreprise code effectif entreprise",
"Entreprise date de création",
"Entreprise état administratif",
"Entreprise nom",
"Entreprise prénom",
"Association RNA",
"Association titre",
"Association objet",
"Association date de création",
"Association date de déclaration",
"Association date de publication"
])
expect(etablissements_sheet.data.size).to eq(2)
expect(dossier_etablissement[1]).to eq("Dossier")
expect(champ_etablissement[1]).to eq("siret")
end
end
describe 'Avis sheet' do
let(:procedure) { create(:procedure, :published, :for_individual) }
let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) }
let!(:avis) { create(:avis, :with_answer, dossier: dossier) }
it 'should have headers and data' do
expect(avis_sheet.headers).to eq([
"Dossier ID",
"Introduction",
"Réponse",
"Question",
"Réponse oui/non",
"Créé le",
"Répondu le",
"Instructeur",
"Expert"
])
expect(avis_sheet.data.size).to eq(1)
end
end
describe 'Repetitions sheet' do
before do
# change one tdc place to check if the header is ordered
tdc_first = procedure.active_revision.revision_types_de_champ_public.first
tdc_last = procedure.active_revision.revision_types_de_champ_public.last
tdc_first.update(position: tdc_last.position + 1)
procedure.reload
end
let(:types_de_champ_public) { [{ type: :repetition, children: [{ libelle: 'Nom' }, { libelle: 'Age' }] }] }
let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) }
let!(:dossiers) do
[
create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure),
create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure)
]
end
let(:champ_repetition) { dossiers.first.champs_public.find { |champ| champ.type_champ == 'repetition' } }
it 'should have sheets' do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.type_de_champ.libelle_for_export])
end
context 'with cloned procedure' do
let(:other_parent) { create(:type_de_champ_repetition, stable_id: champ_repetition.stable_id) }
before do
create(:type_de_champ, parent: create(:procedure_revision_type_de_champ, type_de_champ: other_parent, revision: create(:procedure).active_revision, position: 0))
end
it 'should have headers' do
expect(repetition_sheet.headers).to eq([
"Dossier ID",
"Ligne",
"Nom",
"Age"
])
end
end
it 'should have data' do
expect(repetition_sheet.data.size).to eq(4)
end
context 'with invalid characters' do
before do
champ_repetition.type_de_champ.update(libelle: 'A / B \ C *[]?')
end
it 'should have valid sheet name' do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', "(#{champ_repetition.type_de_champ.stable_id}) A - B - C"])
end
end
context 'with long libelle composed of utf8 characteres' do
before do
procedure.active_revision.types_de_champ_public.each do |c|
c.update!(libelle: "#{c.id} - ?/[] ééé ééé ééééééé ééééééé éééééééé. ééé éé éééééééé éé ééé. ééééé éééééééé ééé ééé.")
end
champ_repetition.champs.each do |c|
c.type_de_champ.update!(libelle: "#{c.id} - Quam rem nam maiores numquam dolorem nesciunt. Cum et possimus et aut. Fugit voluptas qui qui.")
end
end
it 'should have valid sheet name' do
expect { subject }.not_to raise_error
end
end
context 'with non unique labels' do
let(:types_de_champ_public) { [{ type: :repetition, libelle: 'Une repetition', children: [{}] }, { type: :repetition, libelle: 'Une repetition', children: [{}] }] }
let(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure:) }
let(:type_de_champ_repetition) { dossier.revision.types_de_champ_public.first }
let(:another_type_de_champ_repetition) { dossier.revision.types_de_champ_public.second }
it 'should have sheets' do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', type_de_champ_repetition.libelle_for_export, another_type_de_champ_repetition.libelle_for_export])
end
end
context 'with empty repetition' do
before do
dossiers.flat_map { |dossier| dossier.champs_public.filter(&:repetition?) }.each do |champ|
champ.champs.destroy_all
end
end
it 'should not have data' do
expect(repetition_sheet).to be_nil
end
end
end
end
describe 'to_zip' do
let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) }
let(:types_de_champ_public) { [{ type: :piece_justificative, libelle: 'piece_justificative' }] }
subject { service.to_zip }
context 'without files' do
it 'does not raises in_batches' do
expect { subject }.not_to raise_error
end
it 'returns an empty blob' do
expect(subject).to be_an_instance_of(ActiveStorage::Blob)
end
end
describe 'generate_dossiers_export' do
it 'include_infos_administration (so it includes avis, champs privés)' do
expect(ActiveStorage::DownloadableFile).to receive(:create_list_from_dossiers).with(dossiers: anything, user_profile: instructeur, export_template:).and_return([])
subject
end
context 'with export_template' do
let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) }
let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template:).generate_dossiers_export(Dossier.where(id: dossier)) }
let(:export_template) { create(:export_template, :enabled_pjs, groupe_instructeur: procedure.defaut_groupe_instructeur) }
before do
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io")
end
it 'returns a blob with custom filenames' do
VCR.use_cassette('archive/new_file_to_get_200') do
subject
File.write('tmp.zip', subject.download, mode: 'wb')
File.open('tmp.zip') do |fd|
files = ZipTricks::FileReader.read_zip_structure(io: fd)
base_fn = "export"
structure = [
"#{base_fn}/",
"#{base_fn}/dossier-#{dossier.id}/",
"#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}-01.txt",
"#{base_fn}/dossier-#{dossier.id}/export-#{dossier.id}.pdf"
]
expect(files.map(&:filename)).to match_array(structure)
end
FileUtils.remove_entry_secure('tmp.zip')
end
end
end
context 'with files (and http calls)' do
let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) }
let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template: nil).generate_dossiers_export(Dossier.where(id: dossier)) }
before do
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io")
end
it 'returns a blob with valid files' do
VCR.use_cassette('archive/new_file_to_get_200') do
subject
File.write('tmp.zip', subject.download, mode: 'wb')
File.open('tmp.zip') do |fd|
files = ZipTricks::FileReader.read_zip_structure(io: fd)
base_fn = 'export'
structure = [
"#{base_fn}/",
"#{base_fn}/dossier-#{dossier.id}/",
"#{base_fn}/dossier-#{dossier.id}/pieces_justificatives/",
"#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}",
"#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(dossier_exports.first.first)}"
]
expect(files.size).to eq(structure.size)
expect(files.map(&:filename)).to match_array(structure)
end
FileUtils.remove_entry_secure('tmp.zip')
end
end
end
end
end
describe 'to_geo_json' do
let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) }
let(:types_de_champ_public) { [{ type: :carte }] }
subject do
service
.to_geo_json
.open { |f| JSON.parse(f.read) }
end
let(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) }
let(:champ_carte) { dossier.champs_public.find(&:carte?) }
let(:properties) { subject['features'].first['properties'] }
before do
create(:geo_area, :polygon, champ: champ_carte)
end
it 'should have features' do
expect(subject['features'].size).to eq(3)
expect(properties['dossier_id']).to eq(dossier.id)
expect(properties['champ_id']).to eq(champ_carte.stable_id)
expect(properties['champ_label']).to eq(champ_carte.libelle)
end
end
end