demarches-normaliennes/spec/models/concerns/dossier_clone_concern_spec.rb
Paul Chavard 9dd1973e18
fix(dossier): always remove previous champ version
If champ type_de_champ gets out of sync with its type, the persisted champ will not be part of the filled champs collection. In the merge code, we need to remove the previous champ, disregarding its type. The problem should have been caught earlier, but our unique index is not actually unique because our version of PG misses `NULLS NOT DISTINCT`. The unique index only works for champs in repetitions.
2024-12-16 21:49:51 +00:00

481 lines
20 KiB
Ruby

# frozen_string_literal: true
RSpec.describe DossierCloneConcern do
let(:procedure) do
create(:procedure, types_de_champ_public:, types_de_champ_private:).tap(&:publish!)
end
let(:types_de_champ_public) do
[
{ type: :text, libelle: "Un champ text", stable_id: 99 },
{ type: :text, libelle: "Un autre champ text", stable_id: 991 },
{ type: :yes_no, libelle: "Un champ yes no", stable_id: 992 },
{ type: :repetition, libelle: "Un champ répétable", stable_id: 993, mandatory: true, children: [{ type: :text, libelle: 'Nom', stable_id: 994 }] }
]
end
let(:types_de_champ_private) { [] }
let(:dossier) { create(:dossier, :en_construction, procedure:) }
let(:forked_dossier) { dossier.find_or_create_editing_fork(dossier.user) }
describe '#clone' do
let(:dossier) { create(:dossier, :en_construction, :with_populated_champs, procedure:) }
let(:types_de_champ_public) { [{}] }
let(:types_de_champ_private) { [] }
let(:fork) { false }
subject(:new_dossier) { dossier.clone(fork:) }
it 'resets most of the attributes for the cloned dossier' do
expect(new_dossier.id).not_to eq(dossier.id)
expect(new_dossier.api_entreprise_job_exceptions).to be_nil
expect(new_dossier.archived).to be_falsey
expect(new_dossier.brouillon_close_to_expiration_notice_sent_at).to be_nil
expect(new_dossier.conservation_extension).to eq(0.seconds)
expect(new_dossier.declarative_triggered_at).to be_nil
expect(new_dossier.deleted_user_email_never_send).to be_nil
expect(new_dossier.depose_at).to be_nil
expect(new_dossier.en_construction_at).to be_nil
expect(new_dossier.en_construction_close_to_expiration_notice_sent_at).to be_nil
expect(new_dossier.en_instruction_at).to be_nil
expect(new_dossier.for_procedure_preview).to be_falsey
expect(new_dossier.groupe_instructeur_updated_at).to be_nil
expect(new_dossier.hidden_by_administration_at).to be_nil
expect(new_dossier.hidden_by_reason).to be_nil
expect(new_dossier.hidden_by_user_at).to be_nil
expect(new_dossier.identity_updated_at).to be_nil
expect(new_dossier.last_avis_updated_at).to be_nil
expect(new_dossier.last_champ_private_updated_at).to be_nil
expect(new_dossier.last_champ_updated_at).to be_nil
expect(new_dossier.last_champ_piece_jointe_updated_at).to be_nil
expect(new_dossier.last_commentaire_updated_at).to be_nil
expect(new_dossier.last_commentaire_piece_jointe_updated_at).to be_nil
expect(new_dossier.motivation).to be_nil
expect(new_dossier.processed_at).to be_nil
end
it "updates search terms" do
# In spec, dossier and flag reference are created just before deep clone,
# which keep the flag reference from the original, pointing to the original id.
# We have to remove the flag reference before the clone
dossier.remove_instance_variable(:@debounce_index_search_terms_flag_kredis_flag)
perform_enqueued_jobs(only: DossierIndexSearchTermsJob) do
subject
end
sql = "SELECT search_terms, private_search_terms FROM dossiers where id = :id"
result = Dossier.connection.execute(Dossier.sanitize_sql_array([sql, id: new_dossier.id])).first
expect(result["search_terms"]).to match(dossier.user.email)
expect(result["private_search_terms"]).to eq("")
end
context 'copies some attributes' do
context 'when fork' do
let(:fork) { true }
it { expect(new_dossier.groupe_instructeur).to eq(dossier.groupe_instructeur) }
end
context 'when not forked' do
it "copies or reset attributes" do
expect(new_dossier.groupe_instructeur).to be_nil
expect(new_dossier.autorisation_donnees).to eq(dossier.autorisation_donnees)
expect(new_dossier.revision_id).to eq(dossier.revision_id)
expect(new_dossier.user_id).to eq(dossier.user_id)
end
end
end
context 'forces some attributes' do
let(:dossier) { create(:dossier, :accepte) }
it do
expect(new_dossier.brouillon?).to eq(true)
expect(new_dossier.parent_dossier).to eq(dossier)
end
context 'destroy parent' do
before { new_dossier }
it 'clean fk' do
expect { dossier.destroy }.to change { new_dossier.reload.parent_dossier_id }.from(dossier.id).to(nil)
end
end
end
context 'procedure with_individual' do
let(:procedure) { create(:procedure, :for_individual) }
it do
expect(new_dossier.individual.slice(:nom, :prenom, :gender)).to eq(dossier.individual.slice(:nom, :prenom, :gender))
expect(new_dossier.individual.id).not_to eq(dossier.individual.id)
end
end
context 'procedure with etablissement' do
let(:dossier) { create(:dossier, :with_entreprise) }
it do
expect(new_dossier.etablissement.slice(:siret)).to eq(dossier.etablissement.slice(:siret))
expect(new_dossier.etablissement.id).not_to eq(dossier.etablissement.id)
end
end
describe 'champs' do
it { expect(new_dossier.id).not_to eq(dossier.id) }
context 'public are duplicated' do
it do
expect(new_dossier.project_champs_public.count).to eq(dossier.project_champs_public.count)
expect(new_dossier.project_champs_public.map(&:id)).not_to eq(dossier.project_champs_public.map(&:id))
end
it 'keeps champs.values' do
original_first_champ = dossier.project_champs_public.first
original_first_champ.update!(value: 'kthxbye')
expect(new_dossier.project_champs_public.first.value).to eq(original_first_champ.value)
end
context 'for Champs::Repetition with rows, original_champ.repetition and rows are duped' do
let(:types_de_champ_public) { [{ type: :repetition, children: [{}, {}] }] }
let(:champ_repetition) { dossier.project_champs_public.find(&:repetition?) }
let(:cloned_champ_repetition) { new_dossier.project_champs_public.find(&:repetition?) }
it do
expect(cloned_champ_repetition.rows.flatten.count).to eq(4)
expect(cloned_champ_repetition.rows.flatten.map(&:id)).not_to eq(champ_repetition.rows.flatten.map(&:id))
expect(cloned_champ_repetition.row_ids).to eq(champ_repetition.row_ids)
end
end
context 'for Champs::CarteChamp with geo areas, original_champ.geo_areas are duped' do
let(:types_de_champ_public) { [{ type: :carte }] }
let(:champ_carte) { dossier.champs.first }
let(:cloned_champ_carte) { new_dossier.champs.first }
it do
expect(cloned_champ_carte.geo_areas.count).to eq(2)
expect(cloned_champ_carte.geo_areas.ids).not_to eq(champ_carte.geo_areas.ids)
end
end
context 'for Champs::SiretChamp, original_champ.etablissement is duped' do
let(:types_de_champ_public) { [{ type: :siret }] }
let(:champ_siret) { dossier.champs.first }
let(:cloned_champ_siret) { new_dossier.champs.first }
it do
expect(champ_siret.etablissement).not_to be_nil
expect(cloned_champ_siret.etablissement.id).not_to eq(champ_siret.etablissement.id)
end
end
context 'for Champs::PieceJustificative, original_champ.piece_justificative_file is duped' do
let(:types_de_champ_public) { [{ type: :piece_justificative }] }
let(:champ_piece_justificative) { dossier.champs.first }
let(:cloned_champ_piece_justificative) { new_dossier.champs.first }
it { expect(cloned_champ_piece_justificative.piece_justificative_file.first.blob).to eq(champ_piece_justificative.piece_justificative_file.first.blob) }
end
context 'for Champs::AddressChamp, original_champ.data is duped' do
let(:types_de_champ_public) { [{ type: :address }] }
let(:champ_address) { dossier.champs.first }
let(:cloned_champ_address) { new_dossier.champs.first }
before { champ_address.update(external_id: 'Address', data: { city_code: '75019' }) }
it do
expect(champ_address.data).not_to be_nil
expect(champ_address.external_id).not_to be_nil
expect(cloned_champ_address.external_id).to eq(champ_address.external_id)
expect(cloned_champ_address.data).to eq(champ_address.data)
end
end
end
context 'private are renewd' do
let(:types_de_champ_private) { [{}] }
it 'reset champs private values' do
expect(new_dossier.project_champs_private.count).to eq(dossier.project_champs_private.count)
expect(new_dossier.project_champs_private.map(&:id)).not_to eq(dossier.project_champs_private.map(&:id))
original_first_champs_private = dossier.project_champs_private.first
original_first_champs_private.update!(value: 'kthxbye')
expect(new_dossier.project_champs_private.first.value).not_to eq(original_first_champs_private.value)
expect(new_dossier.project_champs_private.first.value).to eq(nil)
end
end
end
context "as a fork" do
let(:new_dossier) { dossier.clone(fork: true) }
before { dossier.project_champs_public } # we compare timestamps so we have to get the precision limit from the db }
it do
expect(new_dossier.editing_fork_origin).to eq(dossier)
expect(new_dossier.project_champs_public[0].id).not_to eq(dossier.project_champs_public[0].id)
expect(new_dossier.project_champs_public[0].created_at).to eq(dossier.project_champs_public[0].created_at)
expect(new_dossier.project_champs_public[0].updated_at).to eq(dossier.project_champs_public[0].updated_at)
end
context "piece justificative champ" do
let(:types_de_champ_public) { [{ type: :piece_justificative }] }
let(:champ_pj) { dossier.champs.first }
let(:cloned_champ_pj) { new_dossier.champs.first }
it {
expect(cloned_champ_pj.piece_justificative_file.first.blob).to eq(champ_pj.piece_justificative_file.first.blob)
expect(cloned_champ_pj.created_at).to eq(champ_pj.created_at)
expect(cloned_champ_pj.updated_at).to eq(champ_pj.updated_at)
}
end
context 'invalid origin' do
let(:procedure) do
create(:procedure, types_de_champ_public: [
{ type: :drop_down_list, libelle: "Le savez-vous?", stable_id: 992, drop_down_options: ["Oui", "Non", "Peut-être"], mandatory: true }
])
end
before do
champ = dossier.champs.find { _1.stable_id == 992 }
champ.value = "Je ne sais pas"
champ.save!(validate: false)
end
it 'can still fork' do
expect(dossier.validate(:champs_public_value)).to be_falsey
new_dossier.champs.load # load relation so champs are validated below
expect(new_dossier.validate(:champs_public_value)).to be_falsey
expect(new_dossier.champs.find { _1.stable_id == 992 }.value).to eq("Je ne sais pas")
end
context 'when associated record is invalid' do
let(:procedure) do
create(:procedure, types_de_champ_public: [
{ type: :carte, libelle: "Carte", stable_id: 992, mandatory: true }
])
end
before do
champ = dossier.champs.find { _1.stable_id == 992 }
geo_area = champ.geo_areas.first
geo_area.geometry = { "i'm" => "invalid" }
geo_area.save!(validate: false)
end
it 'can still fork' do
new_dossier.champs.load # load relation so champs are validated below
expect(new_dossier.champs.find { _1.stable_id == 992 }.geo_areas.first).not_to be_valid
end
end
end
end
end
describe '#make_diff' do
subject { dossier.make_diff(forked_dossier) }
context 'with no changes' do
it { is_expected.to eq(added: [], updated: [], removed: []) }
end
context 'with updated groupe instructeur' do
before {
dossier.update!(groupe_instructeur: create(:groupe_instructeur))
forked_dossier.assign_to_groupe_instructeur(dossier.procedure.defaut_groupe_instructeur, DossierAssignment.modes.fetch(:manual))
}
it do
expect(subject).to eq(added: [], updated: [], removed: [])
expect(forked_dossier.forked_with_changes?).to be_truthy
end
end
context 'with updated champ' do
let(:updated_champ) { forked_dossier.champs.find { _1.stable_id == 99 } }
before { updated_champ.update(value: 'new value') }
it 'forked_with_changes? should reflect dossier state' do
expect(subject).to eq(added: [], updated: [updated_champ], removed: [])
expect(dossier.forked_with_changes?).to be_falsey
expect(forked_dossier.forked_with_changes?).to be_truthy
expect(updated_champ.forked_with_changes?).to be_truthy
end
end
context 'with new revision' do
let(:added_champ) { forked_dossier.project_champs_public.find { _1.libelle == "Un nouveau champ text" } }
let(:removed_champ) { dossier.champs.find { _1.stable_id == 99 } }
let(:new_dossier) { dossier.clone }
before do
procedure.draft_revision.add_type_de_champ({
type_champ: TypeDeChamp.type_champs.fetch(:text),
libelle: "Un nouveau champ text"
})
procedure.draft_revision.remove_type_de_champ(removed_champ.stable_id)
procedure.publish_revision!
end
it {
expect(dossier.revision_id).to eq(procedure.revisions.first.id)
expect(new_dossier.revision_id).to eq(procedure.published_revision.id)
expect(forked_dossier.revision_id).to eq(procedure.published_revision_id)
expect(subject[:added].map(&:stable_id)).to eq([added_champ.stable_id])
expect(subject[:added].first.new_record?).to be_truthy
expect(subject[:updated]).to be_empty
expect(subject[:removed]).to eq([removed_champ])
}
end
end
describe '#merge_fork' do
let(:dossier) { create(:dossier, :en_construction, :with_populated_champs, procedure:) }
subject { dossier.merge_fork(forked_dossier) }
context 'with updated champ' do
let(:repetition_champ) { dossier.project_champs_public.last }
let(:updated_champ) { forked_dossier.champs.find { _1.stable_id == 99 } }
let(:updated_repetition_champs) { forked_dossier.champs.filter { _1.stable_id == 994 } }
before do
repetition_champ.add_row(updated_by: 'test')
dossier.champs.each do |champ|
champ.update(value: 'old value')
end
updated_champ.update(value: 'new value')
updated_repetition_champs.each { _1.update(value: 'new value in repetition') }
dossier.debounce_index_search_terms_flag.remove
end
it { expect { subject }.to change { dossier.champs.reload.size }.by(0) }
it { expect { subject }.not_to change { dossier.champs.order(:created_at).reject { _1.stable_id.in?([99, 994]) }.map(&:value) } }
it { expect { subject }.to have_enqueued_job(DossierIndexSearchTermsJob).with(dossier) }
it { expect { subject }.to change { dossier.champs.find { _1.stable_id == 99 }.value }.from('old value').to('new value') }
it { expect { subject }.to change { dossier.reload.champs.find { _1.stable_id == 994 }.value }.from('old value').to('new value in repetition') }
it 'fork is hidden after merge' do
subject
expect(forked_dossier.reload.hidden_by_reason).to eq("stale_fork")
expect(dossier.reload.editing_forks).to be_empty
end
end
context 'with new revision' do
let(:added_champ) {
tdc = forked_dossier.revision.types_de_champ.find { _1.libelle == "Un nouveau champ text" }
forked_dossier.champ_for_update(tdc, updated_by: 'test')
}
let(:added_repetition_champ) {
tdc_repetition = forked_dossier.revision.types_de_champ.find { _1.stable_id == 993 }
tdc = forked_dossier.revision.types_de_champ.find { _1.libelle == "Texte en répétition" }
row_id = forked_dossier.repetition_row_ids(tdc_repetition).first
forked_dossier.champ_for_update(tdc, row_id:, updated_by: 'test')
}
let(:removed_champ) { dossier.champs.find { _1.stable_id == 99 } }
let(:updated_champ) { dossier.champs.find { _1.stable_id == 991 } }
let(:repetition_updated_champ) { champ_for_update(dossier.champs.find { _1.stable_id == 994 }) }
let(:forked_updated_champ) { champ_for_update(forked_dossier.champs.find { _1.stable_id == 991 }) }
let(:forked_repetition_updated_champ) { champ_for_update(forked_dossier.champs.find { _1.stable_id == 994 }) }
before do
dossier.champs.each do |champ|
champ.update(value: 'old value')
end
dossier.reload
procedure.draft_revision.add_type_de_champ({
type_champ: TypeDeChamp.type_champs.fetch(:text),
libelle: "Un nouveau champ text"
})
procedure.draft_revision.add_type_de_champ({
type_champ: TypeDeChamp.type_champs.fetch(:text),
parent_stable_id: 993,
libelle: "Texte en répétition"
})
procedure.draft_revision.remove_type_de_champ(removed_champ.stable_id)
procedure.draft_revision.find_and_ensure_exclusive_use(updated_champ.stable_id).update(libelle: "Un nouveau libelle")
procedure.publish_revision!
added_champ.update(value: 'new value for added champ')
added_repetition_champ.update(value: "new value in repetition champ")
forked_updated_champ.update(value: 'new value for updated champ')
forked_repetition_updated_champ.update(value: 'new value for updated champ in repetition')
updated_champ.update(type: 'Champs::TextareaChamp')
repetition_updated_champ.update(type: 'Champs::TextareaChamp')
dossier.reload
forked_dossier.reload
end
it { expect { subject }.to change { dossier.filled_champs.size }.by(3) }
it { expect { subject }.to change { dossier.filled_champs.sort_by(&:created_at).map(&:to_s) }.from(['old value', 'Non', 'old value']).to(['new value for updated champ', 'Non', 'new value for updated champ in repetition', 'old value', 'new value for added champ', 'new value in repetition champ']) }
it "dossier after merge should be on last published revision" do
expect(dossier.revision_id).to eq(procedure.revisions.first.id)
expect(forked_dossier.revision_id).to eq(procedure.published_revision_id)
subject
perform_enqueued_jobs only: DestroyRecordLaterJob
expect(dossier.revision_id).to eq(procedure.published_revision_id)
expect(dossier.filled_champs.all? { dossier.revision.in?(_1.type_de_champ.revisions) }).to be_truthy
expect(Dossier.exists?(forked_dossier.id)).to be_falsey
end
end
context 'with old revision having repetition' do
let(:removed_champ) { dossier.project_champs_public.find(&:repetition?) }
before do
dossier.champs.each do |champ|
champ.update(value: 'old value')
end
procedure.draft_revision.remove_type_de_champ(removed_champ.stable_id)
procedure.publish_revision!
end
it 'works' do
expect { subject }.not_to raise_error
end
end
context 'with added row' do
let(:repetition_champ) { forked_dossier.project_champs_public.find(&:repetition?) }
def dossier_rows(dossier) = dossier.champs.filter(&:row?)
before do
repetition_champ.add_row(updated_by: 'test')
end
it {
expect(dossier_rows(dossier).size).to eq(2)
expect { subject }.to change { dossier_rows(dossier).size }.by(1)
}
end
context 'with removed row' do
let(:repetition_champ) { forked_dossier.project_champs_public.find(&:repetition?) }
let(:row_id) { repetition_champ.row_ids.first }
def dossier_rows(dossier) = dossier.champs.filter(&:row?)
def dossier_discarded_rows(dossier) = dossier_rows(dossier).filter(&:discarded?)
before do
repetition_champ.remove_row(row_id, updated_by: 'test')
end
it {
expect(dossier_rows(dossier).size).to eq(2)
expect { subject }.to change { dossier_rows(dossier).size }.by(0)
}
it {
expect(dossier_discarded_rows(dossier).size).to eq(0)
expect { subject }.to change { dossier_discarded_rows(dossier).size }.by(1)
}
end
end
end