From ec8ccad465681f99609911feccb04397ea69b80c Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 12 Oct 2021 11:03:34 +0200 Subject: [PATCH 01/41] typo --- spec/models/expert_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/expert_spec.rb b/spec/models/expert_spec.rb index 5b3ebf607..8d40cdda8 100644 --- a/spec/models/expert_spec.rb +++ b/spec/models/expert_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Expert, type: :model do end it 'transfers the access to the new expert' do - expect(procedure.reload.experts). to match_array(new_expert) + expect(procedure.reload.experts).to match_array(new_expert) end end From 7c65571fca93b4e6030e9bf7a137dd6eeb895432 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 12 Oct 2021 11:04:06 +0200 Subject: [PATCH 02/41] add case when the old_expert or old_instructeur is nil --- app/models/expert.rb | 2 ++ app/models/instructeur.rb | 2 ++ spec/models/expert_spec.rb | 6 ++++++ spec/models/instructeur_spec.rb | 6 ++++++ 4 files changed, 16 insertions(+) diff --git a/app/models/expert.rb b/app/models/expert.rb index 01042c840..4574a474f 100644 --- a/app/models/expert.rb +++ b/app/models/expert.rb @@ -38,6 +38,8 @@ class Expert < ApplicationRecord end def merge(old_expert) + return if old_expert.nil? + procedure_with_new, procedure_without_new = old_expert .procedures .partition { |p| p.experts.exists?(id) } diff --git a/app/models/instructeur.rb b/app/models/instructeur.rb index e9b4c5985..0a3da5b33 100644 --- a/app/models/instructeur.rb +++ b/app/models/instructeur.rb @@ -252,6 +252,8 @@ class Instructeur < ApplicationRecord end def merge(old_instructeur) + return if old_instructeur.nil? + old_instructeur .assign_to .where.not(groupe_instructeur_id: assign_to.pluck(:groupe_instructeur_id)) diff --git a/spec/models/expert_spec.rb b/spec/models/expert_spec.rb index 8d40cdda8..3a27f23aa 100644 --- a/spec/models/expert_spec.rb +++ b/spec/models/expert_spec.rb @@ -19,6 +19,12 @@ RSpec.describe Expert, type: :model do subject { new_expert.merge(old_expert) } + context 'when the old expert does not exist' do + let(:old_expert) { nil } + + it { expect { subject }.not_to raise_error } + end + context 'when an old expert access a procedure' do let(:procedure) { create(:procedure) } diff --git a/spec/models/instructeur_spec.rb b/spec/models/instructeur_spec.rb index a9ffc2c4a..768feef92 100644 --- a/spec/models/instructeur_spec.rb +++ b/spec/models/instructeur_spec.rb @@ -743,6 +743,12 @@ describe Instructeur, type: :model do subject { new_instructeur.merge(old_instructeur) } + context 'when the old instructeur does not exist' do + let(:old_instructeur) { nil } + + it { expect { subject }.not_to raise_error } + end + context 'when an procedure is assigned to the old instructeur' do let(:procedure) { create(:procedure) } From 0b02fce5e49e1002eeceebb9cd3e789e5003113a Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Thu, 7 Oct 2021 13:05:27 +0200 Subject: [PATCH 03/41] jobs: move ActiveJobLogSubscriber out of initializers This is a class of its own, it doesn't need to be in the initializers. --- .../lib/active_job/application_log_subscriber.rb | 2 +- config/initializers/lograge.rb | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) rename config/initializers/active_job_log_subscriber.rb => app/lib/active_job/application_log_subscriber.rb (95%) diff --git a/config/initializers/active_job_log_subscriber.rb b/app/lib/active_job/application_log_subscriber.rb similarity index 95% rename from config/initializers/active_job_log_subscriber.rb rename to app/lib/active_job/application_log_subscriber.rb index 854fc5a61..1f7a3361a 100644 --- a/config/initializers/active_job_log_subscriber.rb +++ b/app/lib/active_job/application_log_subscriber.rb @@ -1,7 +1,7 @@ require 'active_job/logging' require 'logstash-event' -class ActiveJobLogSubscriber < ::ActiveJob::LogSubscriber +class ActiveJob::ApplicationLogSubscriber < ::ActiveJob::LogSubscriber def enqueue(event) process_event(event, 'enqueue') end diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index 5a63f1051..6be2a946b 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -1,5 +1,3 @@ -require_relative './active_job_log_subscriber' - Rails.application.configure do config.lograge.formatter = Lograge::Formatters::Logstash.new config.lograge.base_controller_class = ['ActionController::Base', 'Manager::ApplicationController'] @@ -33,6 +31,6 @@ Rails.application.configure do config.lograge.logger = ActiveSupport::Logger.new(Rails.root.join('log', "logstash_#{Rails.env}.log")) if config.lograge.enabled - ActiveJobLogSubscriber.attach_to(:active_job) + ActiveJob::ApplicationLogSubscriber.attach_to(:active_job) end end From 05e127af4b8382821e4ab41b44e5ba9a0c0438cb Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 22 Sep 2021 15:11:55 +0200 Subject: [PATCH 04/41] corrige une locale --- config/locales/api_particulier.fr.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/locales/api_particulier.fr.yml b/config/locales/api_particulier.fr.yml index 39841906f..352250e17 100644 --- a/config/locales/api_particulier.fr.yml +++ b/config/locales/api_particulier.fr.yml @@ -2,12 +2,14 @@ fr: api_particulier: providers: cnaf: - libelle: Caisse d’allocations familiales (CAF) + libelle: Caisse nationale d’allocations familiales (CAF) scopes: personne: &personne nomPrenom: noms et prénoms dateDeNaissance: date de naissance - sexe: genre + sexe: sexe + M: masculin + F: féminin allocataires: libelle: allocataires <<: *personne From 17a2b5dc53136b20733a359a6fb87feb0c1b9bfc Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 24 Sep 2021 14:22:58 +0200 Subject: [PATCH 05/41] fix a strange encoding error --- app/lib/api_particulier/error.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/api_particulier/error.rb b/app/lib/api_particulier/error.rb index acc810451..4224bcad7 100644 --- a/app/lib/api_particulier/error.rb +++ b/app/lib/api_particulier/error.rb @@ -14,7 +14,7 @@ module APIParticulier msg = <<~TEXT url: #{url} HTTP error code: #{http_error_code} - #{response.body} + #{response.body.force_encoding('UTF-8')} curl message: #{curl_message} total time: #{total_time} connect time: #{connect_time} From b69dafc3d479cb0f4ff316913f817a77bf70aba6 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 5 Oct 2021 14:16:07 +0200 Subject: [PATCH 06/41] CNAF in lowercase --- app/lib/api_particulier/cnaf_adapter.rb | 2 +- config/initializers/inflections.rb | 1 - spec/lib/api_particulier/cnaf_adapter_spec.rb | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/lib/api_particulier/cnaf_adapter.rb b/app/lib/api_particulier/cnaf_adapter.rb index 7a2412d82..5377fecaf 100644 --- a/app/lib/api_particulier/cnaf_adapter.rb +++ b/app/lib/api_particulier/cnaf_adapter.rb @@ -1,4 +1,4 @@ -class APIParticulier::CNAFAdapter +class APIParticulier::CnafAdapter class InvalidSchemaError < ::StandardError def initialize(errors) super(errors.map(&:to_json).join("\n")) diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 1631f421b..c1ecd861e 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -14,7 +14,6 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'JSON' inflect.acronym 'RNA' inflect.acronym 'URL' - inflect.acronym 'CNAF' inflect.irregular 'type_de_champ', 'types_de_champ' inflect.irregular 'type_de_champ_private', 'types_de_champ_private' inflect.irregular 'procedure_revision_type_de_champ', 'procedure_revision_types_de_champ' diff --git a/spec/lib/api_particulier/cnaf_adapter_spec.rb b/spec/lib/api_particulier/cnaf_adapter_spec.rb index 362a0c5f5..db9d534e2 100644 --- a/spec/lib/api_particulier/cnaf_adapter_spec.rb +++ b/spec/lib/api_particulier/cnaf_adapter_spec.rb @@ -1,4 +1,4 @@ -describe APIParticulier::CNAFAdapter do +describe APIParticulier::CnafAdapter do let(:adapter) { described_class.new(api_particulier_token, numero_allocataire, code_postal, requested_sources) } before { stub_const("API_PARTICULIER_URL", "https://particulier-test.api.gouv.fr/api") } @@ -63,7 +63,7 @@ describe APIParticulier::CNAFAdapter do context 'when no sources is requested' do let(:requested_sources) { {} } - it { expect { subject }.to raise_error(APIParticulier::CNAFAdapter::InvalidSchemaError) } + it { expect { subject }.to raise_error(APIParticulier::CnafAdapter::InvalidSchemaError) } end end end From d68129b34de4452426d47a5a37521528033e2560 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 24 Sep 2021 14:21:13 +0200 Subject: [PATCH 07/41] add cnaf type de champ --- app/graphql/schema.graphql | 5 +++++ app/models/type_de_champ.rb | 3 ++- app/models/types_de_champ/cnaf_type_de_champ.rb | 2 ++ config/locales/models/type_de_champ/fr.yml | 2 ++ spec/factories/type_de_champ.rb | 3 +++ spec/services/procedure_export_service_spec.rb | 3 +++ 6 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 app/models/types_de_champ/cnaf_type_de_champ.rb diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 5f73dbc34..24e02e2d3 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -1869,6 +1869,11 @@ enum TypeDeChamp { """ civilite + """ + Données de la Caisse nationale des allocations familiales + """ + cnaf + """ Communes """ diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 2976b6e7d..6511e7f51 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -48,7 +48,8 @@ class TypeDeChamp < ApplicationRecord repetition: 'repetition', titre_identite: 'titre_identite', iban: 'iban', - annuaire_education: 'annuaire_education' + annuaire_education: 'annuaire_education', + cnaf: 'cnaf' } belongs_to :revision, class_name: 'ProcedureRevision', optional: true diff --git a/app/models/types_de_champ/cnaf_type_de_champ.rb b/app/models/types_de_champ/cnaf_type_de_champ.rb new file mode 100644 index 000000000..2702bec84 --- /dev/null +++ b/app/models/types_de_champ/cnaf_type_de_champ.rb @@ -0,0 +1,2 @@ +class TypesDeChamp::CnafTypeDeChamp < TypesDeChamp::TextTypeDeChamp +end diff --git a/config/locales/models/type_de_champ/fr.yml b/config/locales/models/type_de_champ/fr.yml index 9d42446f4..15a4d2aad 100644 --- a/config/locales/models/type_de_champ/fr.yml +++ b/config/locales/models/type_de_champ/fr.yml @@ -36,3 +36,5 @@ fr: titre_identite: 'Titre identité' iban: 'Iban' annuaire_education: 'Annuaire de l’éducation' + cnaf: 'Données de la Caisse nationale des allocations familiales' + diff --git a/spec/factories/type_de_champ.rb b/spec/factories/type_de_champ.rb index ae6fa4cc1..9f5223e86 100644 --- a/spec/factories/type_de_champ.rb +++ b/spec/factories/type_de_champ.rb @@ -151,6 +151,9 @@ FactoryBot.define do factory :type_de_champ_annuaire_education do type_champ { TypeDeChamp.type_champs.fetch(:annuaire_education) } end + factory :type_de_champ_cnaf do + type_champ { TypeDeChamp.type_champs.fetch(:cnaf) } + end factory :type_de_champ_carte do type_champ { TypeDeChamp.type_champs.fetch(:carte) } end diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb index bca98ba9c..6888e77e1 100644 --- a/spec/services/procedure_export_service_spec.rb +++ b/spec/services/procedure_export_service_spec.rb @@ -77,6 +77,7 @@ describe ProcedureExportService do "titre_identite", "iban", "annuaire_education", + "cnaf", "text" ] end @@ -164,6 +165,7 @@ describe ProcedureExportService do "titre_identite", "iban", "annuaire_education", + "cnaf", "text" ] end @@ -247,6 +249,7 @@ describe ProcedureExportService do "titre_identite", "iban", "annuaire_education", + "cnaf", "text" ] end From 354735ace4447eb34e8db23d376679381d6fe75b Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 5 Oct 2021 15:37:13 +0200 Subject: [PATCH 08/41] add champ value_json jsonb column --- app/models/champ.rb | 1 + app/models/champs/address_champ.rb | 1 + app/models/champs/annuaire_education_champ.rb | 1 + app/models/champs/carte_champ.rb | 1 + app/models/champs/checkbox_champ.rb | 1 + app/models/champs/civilite_champ.rb | 1 + app/models/champs/commune_champ.rb | 1 + app/models/champs/date_champ.rb | 1 + app/models/champs/datetime_champ.rb | 1 + app/models/champs/decimal_number_champ.rb | 1 + app/models/champs/departement_champ.rb | 1 + app/models/champs/dossier_link_champ.rb | 1 + app/models/champs/drop_down_list_champ.rb | 1 + app/models/champs/email_champ.rb | 1 + app/models/champs/engagement_champ.rb | 1 + app/models/champs/explication_champ.rb | 1 + app/models/champs/header_section_champ.rb | 1 + app/models/champs/iban_champ.rb | 1 + app/models/champs/integer_number_champ.rb | 1 + app/models/champs/linked_drop_down_list_champ.rb | 1 + app/models/champs/multiple_drop_down_list_champ.rb | 1 + app/models/champs/number_champ.rb | 1 + app/models/champs/pays_champ.rb | 1 + app/models/champs/phone_champ.rb | 1 + app/models/champs/piece_justificative_champ.rb | 1 + app/models/champs/region_champ.rb | 1 + app/models/champs/repetition_champ.rb | 1 + app/models/champs/siret_champ.rb | 1 + app/models/champs/text_champ.rb | 1 + app/models/champs/textarea_champ.rb | 1 + app/models/champs/titre_identite_champ.rb | 1 + app/models/champs/yes_no_champ.rb | 1 + db/migrate/20211005133027_add_value_json_column_to_champ.rb | 5 +++++ db/schema.rb | 1 + 34 files changed, 38 insertions(+) create mode 100644 db/migrate/20211005133027_add_value_json_column_to_champ.rb diff --git a/app/models/champ.rb b/app/models/champ.rb index 539c5af8b..61930b3ab 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/address_champ.rb b/app/models/champs/address_champ.rb index 72da63012..3e7870b50 100644 --- a/app/models/champs/address_champ.rb +++ b/app/models/champs/address_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/annuaire_education_champ.rb b/app/models/champs/annuaire_education_champ.rb index d5a895224..22141ca17 100644 --- a/app/models/champs/annuaire_education_champ.rb +++ b/app/models/champs/annuaire_education_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/carte_champ.rb b/app/models/champs/carte_champ.rb index f8b67c82c..1e91e529e 100644 --- a/app/models/champs/carte_champ.rb +++ b/app/models/champs/carte_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/checkbox_champ.rb b/app/models/champs/checkbox_champ.rb index 40b38af23..82f009577 100644 --- a/app/models/champs/checkbox_champ.rb +++ b/app/models/champs/checkbox_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/civilite_champ.rb b/app/models/champs/civilite_champ.rb index 8f04758cb..ace32a968 100644 --- a/app/models/champs/civilite_champ.rb +++ b/app/models/champs/civilite_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/commune_champ.rb b/app/models/champs/commune_champ.rb index 34569584f..349221cf1 100644 --- a/app/models/champs/commune_champ.rb +++ b/app/models/champs/commune_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/date_champ.rb b/app/models/champs/date_champ.rb index 1eca35bf8..2e09bd29d 100644 --- a/app/models/champs/date_champ.rb +++ b/app/models/champs/date_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/datetime_champ.rb b/app/models/champs/datetime_champ.rb index cf2d6c7e1..f7f5bd496 100644 --- a/app/models/champs/datetime_champ.rb +++ b/app/models/champs/datetime_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/decimal_number_champ.rb b/app/models/champs/decimal_number_champ.rb index b907c1c50..f574c07ca 100644 --- a/app/models/champs/decimal_number_champ.rb +++ b/app/models/champs/decimal_number_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/departement_champ.rb b/app/models/champs/departement_champ.rb index d4eaa8501..ac5c89633 100644 --- a/app/models/champs/departement_champ.rb +++ b/app/models/champs/departement_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/dossier_link_champ.rb b/app/models/champs/dossier_link_champ.rb index 39ba322f8..866f70197 100644 --- a/app/models/champs/dossier_link_champ.rb +++ b/app/models/champs/dossier_link_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/drop_down_list_champ.rb b/app/models/champs/drop_down_list_champ.rb index 29b9be33a..92f52e3f1 100644 --- a/app/models/champs/drop_down_list_champ.rb +++ b/app/models/champs/drop_down_list_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/email_champ.rb b/app/models/champs/email_champ.rb index 67bdd1ca4..c8a1949b0 100644 --- a/app/models/champs/email_champ.rb +++ b/app/models/champs/email_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/engagement_champ.rb b/app/models/champs/engagement_champ.rb index b6992749e..986623136 100644 --- a/app/models/champs/engagement_champ.rb +++ b/app/models/champs/engagement_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/explication_champ.rb b/app/models/champs/explication_champ.rb index 022149978..d20108393 100644 --- a/app/models/champs/explication_champ.rb +++ b/app/models/champs/explication_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/header_section_champ.rb b/app/models/champs/header_section_champ.rb index 983e60b04..0ff9c7659 100644 --- a/app/models/champs/header_section_champ.rb +++ b/app/models/champs/header_section_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/iban_champ.rb b/app/models/champs/iban_champ.rb index feb3429dd..71ba23281 100644 --- a/app/models/champs/iban_champ.rb +++ b/app/models/champs/iban_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/integer_number_champ.rb b/app/models/champs/integer_number_champ.rb index 68bf2e173..8272800e1 100644 --- a/app/models/champs/integer_number_champ.rb +++ b/app/models/champs/integer_number_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/linked_drop_down_list_champ.rb b/app/models/champs/linked_drop_down_list_champ.rb index 157f4af89..f7022d727 100644 --- a/app/models/champs/linked_drop_down_list_champ.rb +++ b/app/models/champs/linked_drop_down_list_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/multiple_drop_down_list_champ.rb b/app/models/champs/multiple_drop_down_list_champ.rb index 5a239151c..87429b0ee 100644 --- a/app/models/champs/multiple_drop_down_list_champ.rb +++ b/app/models/champs/multiple_drop_down_list_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/number_champ.rb b/app/models/champs/number_champ.rb index e0186615c..fa2ec9b47 100644 --- a/app/models/champs/number_champ.rb +++ b/app/models/champs/number_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/pays_champ.rb b/app/models/champs/pays_champ.rb index bddc575ef..634391a31 100644 --- a/app/models/champs/pays_champ.rb +++ b/app/models/champs/pays_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/phone_champ.rb b/app/models/champs/phone_champ.rb index da1166e6b..15002fe60 100644 --- a/app/models/champs/phone_champ.rb +++ b/app/models/champs/phone_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/piece_justificative_champ.rb b/app/models/champs/piece_justificative_champ.rb index 862c57b67..245750c01 100644 --- a/app/models/champs/piece_justificative_champ.rb +++ b/app/models/champs/piece_justificative_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/region_champ.rb b/app/models/champs/region_champ.rb index cf5e89075..255e4abb2 100644 --- a/app/models/champs/region_champ.rb +++ b/app/models/champs/region_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb index 7e9ac54aa..b07fd0958 100644 --- a/app/models/champs/repetition_champ.rb +++ b/app/models/champs/repetition_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/siret_champ.rb b/app/models/champs/siret_champ.rb index 2e21f1ff3..b9b5b27b5 100644 --- a/app/models/champs/siret_champ.rb +++ b/app/models/champs/siret_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/text_champ.rb b/app/models/champs/text_champ.rb index 11fcb3939..1376cce48 100644 --- a/app/models/champs/text_champ.rb +++ b/app/models/champs/text_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/textarea_champ.rb b/app/models/champs/textarea_champ.rb index 89177feb2..028950a5f 100644 --- a/app/models/champs/textarea_champ.rb +++ b/app/models/champs/textarea_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/titre_identite_champ.rb b/app/models/champs/titre_identite_champ.rb index 289546476..982ae7ae7 100644 --- a/app/models/champs/titre_identite_champ.rb +++ b/app/models/champs/titre_identite_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/app/models/champs/yes_no_champ.rb b/app/models/champs/yes_no_champ.rb index ccbd46186..fc8346cfb 100644 --- a/app/models/champs/yes_no_champ.rb +++ b/app/models/champs/yes_no_champ.rb @@ -9,6 +9,7 @@ # row :integer # type :string # value :string +# value_json :jsonb # created_at :datetime # updated_at :datetime # dossier_id :integer diff --git a/db/migrate/20211005133027_add_value_json_column_to_champ.rb b/db/migrate/20211005133027_add_value_json_column_to_champ.rb new file mode 100644 index 000000000..92b9d7a6e --- /dev/null +++ b/db/migrate/20211005133027_add_value_json_column_to_champ.rb @@ -0,0 +1,5 @@ +class AddValueJSONColumnToChamp < ActiveRecord::Migration[6.1] + def change + add_column :champs, :value_json, :jsonb + end +end diff --git a/db/schema.rb b/db/schema.rb index 2d29640fd..cd272f562 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -189,6 +189,7 @@ ActiveRecord::Schema.define(version: 2021_10_06_164955) do t.jsonb "data" t.string "external_id" t.string "fetch_external_data_exceptions", array: true + t.jsonb "value_json" t.index ["dossier_id"], name: "index_champs_on_dossier_id" t.index ["parent_id"], name: "index_champs_on_parent_id" t.index ["private"], name: "index_champs_on_private" From c76d1043fa5029bb82632415b315aaea405de010 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 24 Sep 2021 14:21:30 +0200 Subject: [PATCH 09/41] add cnaf champ --- app/models/champs/cnaf_champ.rb | 42 +++++++++++++++++++++++++++ spec/factories/champ.rb | 4 +++ spec/models/champs/cnaf_champ_spec.rb | 36 +++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 app/models/champs/cnaf_champ.rb create mode 100644 spec/models/champs/cnaf_champ_spec.rb diff --git a/app/models/champs/cnaf_champ.rb b/app/models/champs/cnaf_champ.rb new file mode 100644 index 000000000..2b96815e3 --- /dev/null +++ b/app/models/champs/cnaf_champ.rb @@ -0,0 +1,42 @@ +# == Schema Information +# +# Table name: champs +# +# id :integer not null, primary key +# data :jsonb +# fetch_external_data_exceptions :string is an Array +# private :boolean default(FALSE), not null +# row :integer +# type :string +# value :string +# value_json :jsonb +# created_at :datetime +# updated_at :datetime +# dossier_id :integer +# etablissement_id :integer +# external_id :string +# parent_id :bigint +# type_de_champ_id :integer +# +class Champs::CnafChamp < Champs::TextChamp + store_accessor :value_json, :numero_allocataire, :code_postal + + def fetch_external_data? + true + end + + def fetch_external_data + APIParticulier::CnafAdapter.new( + procedure.api_particulier_token, + numero_allocataire, + code_postal, + procedure.api_particulier_sources + ).to_params + end + + def external_id + if numero_allocataire.present? && code_postal.present? + { code_postal: code_postal, numero_allocataire: numero_allocataire }.to_json + end + end +end diff --git a/spec/factories/champ.rb b/spec/factories/champ.rb index 03a36d695..b438befbb 100644 --- a/spec/factories/champ.rb +++ b/spec/factories/champ.rb @@ -185,6 +185,10 @@ FactoryBot.define do type_de_champ { association :type_de_champ_annuaire_education, procedure: dossier.procedure } end + factory :champ_cnaf, class: 'Champs::CnafChamp' do + type_de_champ { association :type_de_champ_cnaf, procedure: dossier.procedure } + end + factory :champ_siret, class: 'Champs::SiretChamp' do association :type_de_champ, factory: [:type_de_champ_siret] association :etablissement, factory: [:etablissement] diff --git a/spec/models/champs/cnaf_champ_spec.rb b/spec/models/champs/cnaf_champ_spec.rb new file mode 100644 index 000000000..d50f28a6a --- /dev/null +++ b/spec/models/champs/cnaf_champ_spec.rb @@ -0,0 +1,36 @@ +describe Champs::CnafChamp, type: :model do + let(:champ) { described_class.new } + + describe 'numero_allocataire and code_postal' do + before do + champ.numero_allocataire = '1234567' + champ.code_postal = '12345' + end + + it 'saves numero_allocataire and code_postal' do + expect(champ.numero_allocataire).to eq('1234567') + expect(champ.code_postal).to eq('12345') + end + end + + describe 'external_id' do + context 'when only one data is given' do + before do + champ.numero_allocataire = '1234567' + champ.save + end + + it { expect(champ.external_id).to be_nil } + end + + context 'when all data required for an external fetch are given' do + before do + champ.numero_allocataire = '1234567' + champ.code_postal = '12345' + champ.save + end + + it { expect(JSON.parse(champ.external_id)).to eq({ "code_postal" => "12345", "numero_allocataire" => "1234567" }) } + end + end +end From 40d0cfcdc436f69addb5c26d1dc32319ebc8cc6c Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 5 Oct 2021 14:03:30 +0200 Subject: [PATCH 10/41] add champ validation --- app/models/champs/cnaf_champ.rb | 4 ++ config/locales/en.yml | 6 +++ config/locales/fr.yml | 6 +++ config/locales/models/champs/fr.yml | 6 --- spec/models/champs/cnaf_champ_spec.rb | 60 +++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 6 deletions(-) delete mode 100644 config/locales/models/champs/fr.yml diff --git a/app/models/champs/cnaf_champ.rb b/app/models/champs/cnaf_champ.rb index 2b96815e3..e1cb86aff 100644 --- a/app/models/champs/cnaf_champ.rb +++ b/app/models/champs/cnaf_champ.rb @@ -19,6 +19,10 @@ # type_de_champ_id :integer # class Champs::CnafChamp < Champs::TextChamp + # see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/cnaf-input-validation.middleware.ts + validates :numero_allocataire, format: { with: /\A\d{1,7}\z/ }, if: -> { code_postal.present? } + validates :code_postal, format: { with: /\A\w{5}\z/ }, if: -> { numero_allocataire.present? } + store_accessor :value_json, :numero_allocataire, :code_postal def fetch_external_data? diff --git a/config/locales/en.yml b/config/locales/en.yml index 0224de480..5c6bccab0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -274,6 +274,12 @@ en: taken: is already used for procedure. You cannot use it because it belongs to another administrator. # taken_can_be_claimed: est identique à celui d’une autre de vos procedures publiées. Si vous publiez cette procedure, l’ancienne sera dépubliée et ne sera plus accessible au public. Les utilisateurs qui ont commencé un brouillon vont pouvoir le déposer. invalid: is not valid. It must countain between 3 and 50 characters among a-z, 0-9, '_' and '-'. + "champs/cnaf_champ": + attributes: + numero_allocataire: + invalid: "must be a maximum of 7 digits" + code_postal: + invalid: "must be 5 characters long" errors: messages: dossier_not_found: "The file does not exist or you do not have access to it." diff --git a/config/locales/fr.yml b/config/locales/fr.yml index cf9eef404..d2361db5a 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -280,6 +280,12 @@ fr: taken: est déjà utilisé par une démarche. Vous ne pouvez pas l’utiliser car il appartient à un autre administrateur. taken_can_be_claimed: est identique à celui d’une autre de vos démarches publiées. Si vous publiez cette démarche, l’ancienne sera dépubliée et ne sera plus accessible au public. Les utilisateurs qui ont commencé un brouillon vont pouvoir le déposer. invalid: n’est pas valide. Il doit comporter au moins 3 caractères, au plus 50 caractères et seuls les caractères a-z, 0-9, '_' et '-' sont autorisés. + "champs/cnaf_champ": + attributes: + numero_allocataire: + invalid: "doit être composé au maximum de 7 chiffres" + code_postal: + invalid: "doit posséder 5 caractères" errors: messages: saml_not_authorized: "Vous n’êtes pas autorisé à accéder à ce service." diff --git a/config/locales/models/champs/fr.yml b/config/locales/models/champs/fr.yml deleted file mode 100644 index 7a0730a32..000000000 --- a/config/locales/models/champs/fr.yml +++ /dev/null @@ -1,6 +0,0 @@ -fr: - activerecord: - attributes: - champ: - value: La valeur du champ - piece_justificative_file: La pièce justificative diff --git a/spec/models/champs/cnaf_champ_spec.rb b/spec/models/champs/cnaf_champ_spec.rb index d50f28a6a..924e9fffe 100644 --- a/spec/models/champs/cnaf_champ_spec.rb +++ b/spec/models/champs/cnaf_champ_spec.rb @@ -33,4 +33,64 @@ describe Champs::CnafChamp, type: :model do it { expect(JSON.parse(champ.external_id)).to eq({ "code_postal" => "12345", "numero_allocataire" => "1234567" }) } end end + + describe '#validate' do + let(:numero_allocataire) { '1234567' } + let(:code_postal) { '12345' } + let(:champ) { described_class.new(dossier: create(:dossier), type_de_champ: create(:type_de_champ_cnaf)) } + + subject { champ.valid? } + + before do + champ.numero_allocataire = numero_allocataire + champ.code_postal = code_postal + end + + context 'when numero_allocataire and code_postal are valids' do + it { is_expected.to be true } + end + + context 'when numero_allocataire and code_postal are nil' do + let(:numero_allocataire) { nil } + let(:code_postal) { nil } + + it { is_expected.to be true } + end + + context 'when only code_postal is nil' do + let(:code_postal) { nil } + + it do + is_expected.to be false + expect(champ.errors.full_messages).to eq(["Code postal doit posséder 5 caractères"]) + end + end + + context 'when only numero_allocataire is nil' do + let(:numero_allocataire) { nil } + + it do + is_expected.to be false + expect(champ.errors.full_messages).to eq(["Numero allocataire doit être composé au maximum de 7 chiffres"]) + end + end + + context 'when numero_allocataire is invalid' do + let(:numero_allocataire) { '123456a' } + + it do + is_expected.to be false + expect(champ.errors.full_messages).to eq(["Numero allocataire doit être composé au maximum de 7 chiffres"]) + end + end + + context 'when code_postal is invalid' do + let(:code_postal) { '123456' } + + it do + is_expected.to be false + expect(champ.errors.full_messages).to eq(["Code postal doit posséder 5 caractères"]) + end + end + end end From 57a7f82a8f650c6dbce798bf537ab350db962ab9 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 21 Sep 2021 14:49:54 +0200 Subject: [PATCH 11/41] add cnaf ui --- app/assets/stylesheets/cnaf.scss | 30 +++++++++++++++++++ app/assets/stylesheets/forms.scss | 11 +++++++ app/models/champs/cnaf_champ.rb | 4 +++ .../shared/champs/cnaf/_adresse.html.haml | 10 +++++++ .../shared/champs/cnaf/_personnes.html.haml | 19 ++++++++++++ .../champs/cnaf/_quotient_familial.html.haml | 14 +++++++++ app/views/shared/champs/cnaf/_show.html.haml | 24 +++++++++++++++ .../shared/dossiers/_champ_row.html.haml | 2 ++ .../dossiers/editable_champs/_cnaf.html.haml | 16 ++++++++++ config/locales/shared.fr.yml | 16 ++++++++++ 10 files changed, 146 insertions(+) create mode 100644 app/assets/stylesheets/cnaf.scss create mode 100644 app/views/shared/champs/cnaf/_adresse.html.haml create mode 100644 app/views/shared/champs/cnaf/_personnes.html.haml create mode 100644 app/views/shared/champs/cnaf/_quotient_familial.html.haml create mode 100644 app/views/shared/champs/cnaf/_show.html.haml create mode 100644 app/views/shared/dossiers/editable_champs/_cnaf.html.haml create mode 100644 config/locales/shared.fr.yml diff --git a/app/assets/stylesheets/cnaf.scss b/app/assets/stylesheets/cnaf.scss new file mode 100644 index 000000000..9cbd7ea36 --- /dev/null +++ b/app/assets/stylesheets/cnaf.scss @@ -0,0 +1,30 @@ +@import "constants"; +@import "colors"; + +table.cnaf { + margin: 2 * $default-padding 0 $default-padding $default-padding; + width: 100%; + + caption { + font-weight: bold; + margin-left: - $default-padding; + margin-bottom: $default-spacer; + text-align: left; + } + + th, + td { + font-weight: normal; + padding: $default-spacer; + } + + th.text-right { + text-align: right; + } + + &.horizontal { + th { + border-bottom: 1px solid $grey; + } + } +} diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index a21886db7..717b9b51e 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -356,6 +356,17 @@ } } + .cnaf-inputs { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + max-width: 700px; + + input { + width: inherit; + } + } + input.aa-input, input.aa-hint { border-radius: 4px; diff --git a/app/models/champs/cnaf_champ.rb b/app/models/champs/cnaf_champ.rb index e1cb86aff..586430877 100644 --- a/app/models/champs/cnaf_champ.rb +++ b/app/models/champs/cnaf_champ.rb @@ -25,6 +25,10 @@ class Champs::CnafChamp < Champs::TextChamp store_accessor :value_json, :numero_allocataire, :code_postal + def blank? + external_id.nil? + end + def fetch_external_data? true end diff --git a/app/views/shared/champs/cnaf/_adresse.html.haml b/app/views/shared/champs/cnaf/_adresse.html.haml new file mode 100644 index 000000000..9c3cbdd1e --- /dev/null +++ b/app/views/shared/champs/cnaf/_adresse.html.haml @@ -0,0 +1,10 @@ +%table.cnaf + %caption #{t("api_particulier.providers.cnaf.scopes.adresse.libelle")} : + - for key in ['identite', 'complementIdentite', 'numeroRue', 'complementIdentiteGeo', 'lieuDit', 'codePostalVille', 'pays'] do + - if adresse[key].present? + %tr + %th= t("api_particulier.providers.cnaf.scopes.adresse.#{key}") + %td= adresse[key] + + + diff --git a/app/views/shared/champs/cnaf/_personnes.html.haml b/app/views/shared/champs/cnaf/_personnes.html.haml new file mode 100644 index 000000000..d4fa3ee95 --- /dev/null +++ b/app/views/shared/champs/cnaf/_personnes.html.haml @@ -0,0 +1,19 @@ +%table.cnaf.horizontal + %caption #{t("api_particulier.providers.cnaf.scopes.#{scope}.libelle")} : + %thead + %tr + - for key in ['nomPrenom', 'sexe', 'dateDeNaissance'] do + - if personnes.first[key].present? + %th{ class: "#{"text-right" if key == 'dateDeNaissance'}" }= t("api_particulier.providers.cnaf.scopes.personne.#{key}") + %tbody + - personnes.each do |personne| + %tr + - for key in ['nomPrenom', 'sexe', 'dateDeNaissance'] do + - if personne[key].present? + - case key + - when 'dateDeNaissance' + %td.text-right= try_format_datetime(Date.strptime(personne[key], "%d%m%Y")) + - when 'sexe' + %td= t("api_particulier.providers.cnaf.scopes.personne.#{personne[key]}") + - else + %td= personne[key] diff --git a/app/views/shared/champs/cnaf/_quotient_familial.html.haml b/app/views/shared/champs/cnaf/_quotient_familial.html.haml new file mode 100644 index 000000000..b01d09ee7 --- /dev/null +++ b/app/views/shared/champs/cnaf/_quotient_familial.html.haml @@ -0,0 +1,14 @@ +%table.cnaf.horizontal + %caption #{t("api_particulier.providers.cnaf.scopes.quotient_familial.libelle")} : + %thead + %tr + - for key in ['quotientFamilial', 'mois', 'annee'] do + - if quotient_familial[key].present? + %th.text-right= t("api_particulier.providers.cnaf.scopes.quotient_familial.#{key}") + %tbody + %tr + - for key in ['quotientFamilial', 'mois', 'annee'] do + - if quotient_familial[key].present? + %td.text-right= quotient_familial[key] + - else + %td diff --git a/app/views/shared/champs/cnaf/_show.html.haml b/app/views/shared/champs/cnaf/_show.html.haml new file mode 100644 index 000000000..82a7333c6 --- /dev/null +++ b/app/views/shared/champs/cnaf/_show.html.haml @@ -0,0 +1,24 @@ +- if champ.blank? + %p= t('.not_filled') +- elsif champ.data.blank? + %p= t('.fetching_data', + numero_allocataire: champ.numero_allocataire, + code_postal: champ.code_postal) +- else + - if profile == 'usager' + %p= t('.data_fetched', + sources: champ.procedure.api_particulier_sources['cnaf'].keys.map(&:to_s).join(', '), + numero_allocataire: champ.numero_allocataire, + code_postal: champ.code_postal) + + - if profile == 'instructeur' + %p= t('.data_fetched_title') + + - ['adresse', 'quotient_familial', 'enfants', 'allocataires'].each do |scope| + - if champ.data[scope].present? + - if scope == 'quotient_familial' + = render partial: 'shared/champs/cnaf/quotient_familial', locals: { quotient_familial: champ.data[scope] } + - if scope.in? ['enfants', 'allocataires'] + = render partial: 'shared/champs/cnaf/personnes', locals: { scope: scope, personnes: champ.data[scope] } + - elsif scope == 'adresse' + = render partial: 'shared/champs/cnaf/adresse', locals: { adresse: champ.data[scope] } diff --git a/app/views/shared/dossiers/_champ_row.html.haml b/app/views/shared/dossiers/_champ_row.html.haml index 9f4057894..0264327cf 100644 --- a/app/views/shared/dossiers/_champ_row.html.haml +++ b/app/views/shared/dossiers/_champ_row.html.haml @@ -36,6 +36,8 @@ = render partial: "shared/champs/textarea/show", locals: { champ: c } - when TypeDeChamp.type_champs.fetch(:annuaire_education) = render partial: "shared/champs/annuaire_education/show", locals: { champ: c } + - when TypeDeChamp.type_champs.fetch(:cnaf) + = render partial: "shared/champs/cnaf/show", locals: { champ: c, profile: profile } - when TypeDeChamp.type_champs.fetch(:address) = render partial: "shared/champs/address/show", locals: { champ: c } - when TypeDeChamp.type_champs.fetch(:communes) diff --git a/app/views/shared/dossiers/editable_champs/_cnaf.html.haml b/app/views/shared/dossiers/editable_champs/_cnaf.html.haml new file mode 100644 index 000000000..1d8d90610 --- /dev/null +++ b/app/views/shared/dossiers/editable_champs/_cnaf.html.haml @@ -0,0 +1,16 @@ +.cnaf-inputs + %div + = form.label :numero_allocataire, t('.numero_allocataire_label') + %p.notice= t('.numero_allocataire_notice') + = form.text_field :numero_allocataire, + required: champ.mandatory?, + size: 7, + aria: { describedby: describedby_id(champ) } + + %div + = form.label :code_postal, t('.code_postal_label') + %p.notice= t('.code_postal_notice') + = form.text_field :code_postal, + size: 5, + required: champ.mandatory?, + aria: { describedby: describedby_id(champ) } diff --git a/config/locales/shared.fr.yml b/config/locales/shared.fr.yml new file mode 100644 index 000000000..a0bf159e1 --- /dev/null +++ b/config/locales/shared.fr.yml @@ -0,0 +1,16 @@ +fr: + shared: + dossiers: + editable_champs: + cnaf: + numero_allocataire_label: Le numéro d’allocataire CAF + numero_allocataire_notice: Il est généralement composé de 7 chiffres. + code_postal_label: Le code postal + code_postal_notice: Il est généralement composé de 5 chiffres. + champs: + cnaf: + show: + not_filled: non renseigné + fetching_data: "La récupération automatique des données pour l’allocataire Nº %{numero_allocataire} avec le code postal %{code_postal} est en cours." + data_fetched: "Des données concernant %{sources} liées au compte Nº %{numero_allocataire} avec le code postal %{code_postal} ont été reçues depuis la CAF." + data_fetched_title: "Données obtenues de la Caisse nationale d’allocations familiales" From 8c81558e565e8edd5f4bf98419e03ac5dfe606b7 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 27 Sep 2021 17:34:18 +0200 Subject: [PATCH 12/41] english locales --- config/locales/api_particulier.en.yml | 32 +++++++++++++++++++++++++++ config/locales/shared.en.yml | 16 ++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 config/locales/api_particulier.en.yml create mode 100644 config/locales/shared.en.yml diff --git a/config/locales/api_particulier.en.yml b/config/locales/api_particulier.en.yml new file mode 100644 index 000000000..6c0abf1b5 --- /dev/null +++ b/config/locales/api_particulier.en.yml @@ -0,0 +1,32 @@ +en: + api_particulier: + providers: + cnaf: + libelle: Caisse nationale d’allocations familiales (CAF) + scopes: + personne: &personne + nomPrenom: first and last name + dateDeNaissance: birth date + sexe: sex + M: male + F: female + allocataires: + libelle: beneficiaries + <<: *personne + enfants: + libelle: children + <<: *personne + adresse: + libelle: address + identite: identity + complementIdentite: complément d’identité + complementIdentiteGeo: complément d’identité géographique + numeroRue: number and street + lieuDit: lieu-dit + codePostalVille: postcode and city + pays: country + quotient_familial: + libelle: quotient familial + quotientFamilial: quotient familial + mois: month + annee: year diff --git a/config/locales/shared.en.yml b/config/locales/shared.en.yml new file mode 100644 index 000000000..7ef3d6294 --- /dev/null +++ b/config/locales/shared.en.yml @@ -0,0 +1,16 @@ +en: + shared: + dossiers: + editable_champs: + cnaf: + numero_allocataire_label: CAF benefit number + numero_allocataire_notice: It is usually composed of 7 digits. + code_postal_label: postal code + code_postal_notice: It is usually composed of 5 digits. + champs: + cnaf: + show: + not_filled: not filled + fetching_data: "Fetching data for recipient No. %{numero_allocataire} with postal code %{code_postal}." + data_fetched: "Data concerning %{sources} linked to the account Nº %{numero_allocataire} with the postal code %{code_postal} has been received from the CAF." + data_fetched_title: "Data received from la Caisse nationale d’allocations familiales" From 7072993721979be7f9de532b21fe91bfd28d7369 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 24 Sep 2021 14:22:13 +0200 Subject: [PATCH 13/41] a form can upload numero_allocataire and code_postal --- app/controllers/users/dossiers_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 45659b21a..31fcb4b82 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -337,8 +337,8 @@ module Users def champs_params params.permit(dossier: { champs_attributes: [ - :id, :value, :external_id, :primary_value, :secondary_value, :piece_justificative_file, value: [], - champs_attributes: [:id, :_destroy, :value, :external_id, :primary_value, :secondary_value, :piece_justificative_file, value: []] + :id, :value, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: [], + champs_attributes: [:id, :_destroy, :value, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: []] ] }) end From 7aee944daac9e9b1ad991f5a1abe5c489ed3965c Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 27 Sep 2021 16:18:22 +0200 Subject: [PATCH 14/41] show cnaf tdc when procedure is compatible --- app/models/procedure.rb | 4 ++++ app/models/type_de_champ.rb | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/models/procedure.rb b/app/models/procedure.rb index e74f8f47d..df9aba673 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -716,6 +716,10 @@ class Procedure < ApplicationRecord published_revision.touch(:published_at) end + def cnaf_enabled? + api_particulier_sources['cnaf'].present? + end + private def before_publish diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 6511e7f51..dcf5b096d 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -292,17 +292,26 @@ class TypeDeChamp < ApplicationRecord def self.type_de_champ_types_for(procedure, user) has_legacy_number = (procedure.types_de_champ + procedure.types_de_champ_private).any?(&:legacy_number?) - show_number = -> (tdc) { tdc != TypeDeChamp.type_champs.fetch(:number) || has_legacy_number } - - enabled_featured_champ = -> (tdc) do + filter_featured_tdc = -> (tdc) do feature_name = FEATURE_FLAGS[tdc] feature_name.blank? || Flipper.enabled?(feature_name, user) end + filter_tdc = -> (tdc) do + case tdc + when TypeDeChamp.type_champs.fetch(:number) + has_legacy_number + when TypeDeChamp.type_champs.fetch(:cnaf) + procedure.cnaf_enabled? + else + true + end + end + type_champs .keys - .filter(&show_number) - .filter(&enabled_featured_champ) + .filter(&filter_tdc) + .filter(&filter_featured_tdc) .map { |tdc| [I18n.t("activerecord.attributes.type_de_champ.type_champs.#{tdc}"), tdc] } .sort_by(&:first) end From ecc26897e2fab2cb037df7cc2a5c7c32087d9597 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 28 Sep 2021 15:11:15 +0200 Subject: [PATCH 15/41] add end to end spec --- .../api_particulier.html.haml | 2 +- .../procedures/show.html.haml | 2 +- .../sources_particulier/show.html.haml | 2 +- spec/factories/procedure.rb | 6 + .../api_particulier/api_particulier_spec.rb | 158 ++++++++++++++++++ spec/features/routing/full_scenario_spec.rb | 11 -- spec/support/feature_helpers.rb | 11 ++ 7 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 spec/features/api_particulier/api_particulier_spec.rb diff --git a/app/views/new_administrateur/jeton_particulier/api_particulier.html.haml b/app/views/new_administrateur/jeton_particulier/api_particulier.html.haml index ad8f7bc14..40f23fa2c 100644 --- a/app/views/new_administrateur/jeton_particulier/api_particulier.html.haml +++ b/app/views/new_administrateur/jeton_particulier/api_particulier.html.haml @@ -5,7 +5,7 @@ .container .flex - = link_to admin_procedure_api_particulier_jeton_path, class: 'card-admin' do + = link_to admin_procedure_api_particulier_jeton_path, class: 'card-admin', id: 'add-jeton' do - if @procedure.api_particulier_token.blank? %div %span.icon.clock diff --git a/app/views/new_administrateur/procedures/show.html.haml b/app/views/new_administrateur/procedures/show.html.haml index d0aa14694..40d26f61e 100644 --- a/app/views/new_administrateur/procedures/show.html.haml +++ b/app/views/new_administrateur/procedures/show.html.haml @@ -193,7 +193,7 @@ %p.button Modifier - if feature_enabled?(:api_particulier) - = link_to admin_procedure_api_particulier_path(@procedure), class: 'card-admin' do + = link_to admin_procedure_api_particulier_path(@procedure), class: 'card-admin', id: 'api-particulier' do - if @procedure.api_particulier_token.present? %div %span.icon.accept diff --git a/app/views/new_administrateur/sources_particulier/show.html.haml b/app/views/new_administrateur/sources_particulier/show.html.haml index cd06d96c6..6f839d482 100644 --- a/app/views/new_administrateur/sources_particulier/show.html.haml +++ b/app/views/new_administrateur/sources_particulier/show.html.haml @@ -16,7 +16,7 @@ - scopes.each do |scope_key, sources| %h3.explication-libelle= t("api_particulier.providers.#{provider_key}.scopes.#{scope_key}.libelle") - %ul.procedure-admin-api-particulier-sources + %ul.procedure-admin-api-particulier-sources{ id: scope_key } - sources.each do |source_key, enabled_hash| - enabled = (@procedure.api_particulier_sources.dig(provider_key, scope_key)&.include?(source_key)).present? %li diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb index 2651a971e..47a85de6a 100644 --- a/spec/factories/procedure.rb +++ b/spec/factories/procedure.rb @@ -205,6 +205,12 @@ FactoryBot.define do end end + trait :with_cnaf do + after(:build) do |procedure, _evaluator| + build(:type_de_champ_cnaf, procedure: procedure) + end + end + trait :with_explication do after(:build) do |procedure, _evaluator| build(:type_de_champ_explication, procedure: procedure) diff --git a/spec/features/api_particulier/api_particulier_spec.rb b/spec/features/api_particulier/api_particulier_spec.rb new file mode 100644 index 000000000..ce7b1cbf0 --- /dev/null +++ b/spec/features/api_particulier/api_particulier_spec.rb @@ -0,0 +1,158 @@ +feature 'fetch API Particulier Data', js: true do + let(:administrateur) { create(:administrateur) } + + let(:expected_token) { 'd7e9c9f4c3ca00caadde31f50fd4521a' } + + let(:expected_sources) do + { + "cnaf" => + { + "adresse" => ["identite", "complementIdentite", "complementIdentiteGeo", "numeroRue", "lieuDit", "codePostalVille", "pays"], + "allocataires" => ["nomPrenom", "dateDeNaissance", "sexe"], + "enfants" => ["nomPrenom", "dateDeNaissance", "sexe"], + "quotient_familial" => ["quotientFamilial", "annee", "mois"] + } + } + end + + before do + stub_const("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api") + Flipper.enable(:api_particulier) + end + + context "when an administrateur is logged" do + let(:procedure) do + create(:procedure, :with_service, :with_instructeur, + aasm_state: :brouillon, + administrateurs: [administrateur], + libelle: "libellé de la procédure", + path: "libelle-de-la-procedure") + end + + before { login_as administrateur.user, scope: :user } + + scenario 'it can enable api particulier' do + visit admin_procedure_path(procedure) + expect(page).to have_content("Configurer le jeton API particulier") + + find('#api-particulier').click + expect(page).to have_current_path(admin_procedure_api_particulier_path(procedure)) + + find('#add-jeton').click + expect(page).to have_current_path(admin_procedure_api_particulier_jeton_path(procedure)) + + fill_in 'procedure_api_particulier_token', with: expected_token + VCR.use_cassette("api_particulier/success/introspect") { click_on 'Enregistrer' } + expect(page).to have_text('Le jeton a bien été mis à jour') + expect(page).to have_current_path(admin_procedure_api_particulier_sources_path(procedure)) + + ['allocataires', 'enfants'].each do |scope| + within("##{scope}") do + check('noms et prénoms') + check('date de naissance') + check('sexe') + end + end + + within("#adresse") do + check('identité') + check('complément d’identité') + check('complément d’identité géographique') + check('numéro et rue') + check('lieu-dit') + check('code postal et ville') + check('pays') + end + + within("#quotient_familial") do + check('quotient familial') + check('année') + check('mois') + end + + click_on "Enregistrer" + + within("#enfants") do + expect(find('input[value=nomPrenom]')).to be_checked + end + + expect(procedure.reload.api_particulier_sources).to eq(expected_sources) + + visit champs_admin_procedure_path(procedure) + + add_champ + select('Données de la Caisse nationale des allocations familiales', from: 'champ-0-type_champ') + fill_in 'champ-0-libelle', with: 'libellé de champ' + blur + expect(page).to have_content('Formulaire enregistré') + + visit admin_procedure_path(procedure) + find('#publish-procedure-link').click + expect(find_field('procedure_path').value).to eq procedure.path + fill_in 'lien_site_web', with: 'http://some.website' + click_on 'Publier' + + expect(page).to have_text('Démarche publiée') + end + end + + context 'when an user is logged' do + let(:user) { create(:user) } + let(:api_particulier_token) { '29eb50b65f64e8e00c0847a8bbcbd150e1f847' } + let(:numero_allocataire) { '5843972' } + let(:code_postal) { '92110' } + let(:instructeur) { create(:instructeur) } + + let(:procedure) do + create(:procedure, :for_individual, :with_service, :with_cnaf, :published, + libelle: "libellé de la procédure", + path: "libelle-de-la-procedure", + instructeurs: [instructeur], + api_particulier_sources: expected_sources, + api_particulier_token: api_particulier_token) + end + + before { login_as user, scope: :user } + + scenario 'it can fill an cnaf champ' do + visit commencer_path(path: procedure.path) + click_on 'Commencer la démarche' + + choose 'Monsieur' + fill_in 'individual_nom', with: 'Nom' + fill_in 'individual_prenom', with: 'Prenom' + + click_button('Continuer') + + fill_in 'Le numéro d’allocataire CAF', with: numero_allocataire + fill_in 'Le code postal', with: code_postal + + VCR.use_cassette("api_particulier/success/composition_familiale") do + perform_enqueued_jobs { click_on 'Déposer le dossier' } + end + + dossier = Dossier.last + + visit demande_dossier_path(dossier) + expect(page).to have_content(/Des données.*ont été reçues depuis la CAF/) + + log_out + + login_as instructeur.user, scope: :user + + visit instructeur_dossier_path(procedure, dossier) + + expect(page).to have_content('code postal et ville 92110 Clichy') + expect(page).to have_content('identité Mr SNOW Eric') + expect(page).to have_content('complément d’identité ne connait rien') + expect(page).to have_content('numéro et rue 109 rue La Boétie') + expect(page).to have_content('pays FRANCE') + expect(page).to have_content('complément d’identité géographique au nord de paris') + expect(page).to have_content('lieu-dit glagla') + expect(page).to have_content('ERIC SNOW masculin 07/01/1991') + expect(page).to have_content('SANSA SNOW féminin 15/01/1992') + expect(page).to have_content('PAUL SNOW masculin 04/01/2018') + expect(page).to have_content('1856 6 2021') + end + end +end diff --git a/spec/features/routing/full_scenario_spec.rb b/spec/features/routing/full_scenario_spec.rb index c09c3f706..ffdf53704 100644 --- a/spec/features/routing/full_scenario_spec.rb +++ b/spec/features/routing/full_scenario_spec.rb @@ -222,15 +222,4 @@ feature 'The routing', js: true do expect(page).to have_text('Mot de passe enregistré') end - - def log_out(old_layout: false) - if old_layout - page.all('.dropdown-button').first.click - click_on 'Se déconnecter' - else - click_button(title: 'Mon compte') - click_on 'Se déconnecter' - end - expect(page).to have_current_path(root_path) - end end diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index 5b63e9386..2ecd89893 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -142,6 +142,17 @@ module FeatureHelpers have_css("##{form_id_for(libelle)}[value=\"#{with}\"]") end + def log_out(old_layout: false) + if old_layout + page.all('.dropdown-button').first.click + click_on 'Se déconnecter' + else + click_button(title: 'Mon compte') + click_on 'Se déconnecter' + end + expect(page).to have_current_path(root_path) + end + # Keep the brower window open after a test success of failure, to # allow inspecting the page or the console. # From ac60d6c5a1b09acc771f64e1e14798267c5c09e7 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 28 Sep 2021 15:11:58 +0200 Subject: [PATCH 16/41] homogennize api particulier endpoint test url --- .../cassettes/api_particulier/success/composition_familiale.yml | 2 +- .../api_particulier/success/composition_familiale_invalid.yml | 2 +- spec/lib/api_particulier/cnaf_adapter_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/fixtures/cassettes/api_particulier/success/composition_familiale.yml b/spec/fixtures/cassettes/api_particulier/success/composition_familiale.yml index 7732aaf4d..22497f8a6 100644 --- a/spec/fixtures/cassettes/api_particulier/success/composition_familiale.yml +++ b/spec/fixtures/cassettes/api_particulier/success/composition_familiale.yml @@ -2,7 +2,7 @@ http_interactions: - request: method: get - uri: https://particulier-test.api.gouv.fr/api/v2/composition-familiale?codePostal=92110&numeroAllocataire=5843972 + uri: https://particulier.api.gouv.fr/api/v2/composition-familiale?codePostal=92110&numeroAllocataire=5843972 body: encoding: US-ASCII string: '' diff --git a/spec/fixtures/cassettes/api_particulier/success/composition_familiale_invalid.yml b/spec/fixtures/cassettes/api_particulier/success/composition_familiale_invalid.yml index e562a0871..49b3f8ccf 100644 --- a/spec/fixtures/cassettes/api_particulier/success/composition_familiale_invalid.yml +++ b/spec/fixtures/cassettes/api_particulier/success/composition_familiale_invalid.yml @@ -2,7 +2,7 @@ http_interactions: - request: method: get - uri: https://particulier-test.api.gouv.fr/api/v2/composition-familiale?codePostal=92110&numeroAllocataire=5843972 + uri: https://particulier.api.gouv.fr/api/v2/composition-familiale?codePostal=92110&numeroAllocataire=5843972 body: encoding: US-ASCII string: '' diff --git a/spec/lib/api_particulier/cnaf_adapter_spec.rb b/spec/lib/api_particulier/cnaf_adapter_spec.rb index db9d534e2..bd55a2a5f 100644 --- a/spec/lib/api_particulier/cnaf_adapter_spec.rb +++ b/spec/lib/api_particulier/cnaf_adapter_spec.rb @@ -1,7 +1,7 @@ describe APIParticulier::CnafAdapter do let(:adapter) { described_class.new(api_particulier_token, numero_allocataire, code_postal, requested_sources) } - before { stub_const("API_PARTICULIER_URL", "https://particulier-test.api.gouv.fr/api") } + before { stub_const("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api") } describe '#to_params' do let(:api_particulier_token) { '29eb50b65f64e8e00c0847a8bbcbd150e1f847' } From 35c7f05a0a99e57c18cd7039f077dc4c1cec781c Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 28 Sep 2021 15:12:10 +0200 Subject: [PATCH 17/41] source service supports unknown scope --- app/lib/api_particulier/services/sources_service.rb | 1 + spec/lib/api_particulier/services/sources_service_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/lib/api_particulier/services/sources_service.rb b/app/lib/api_particulier/services/sources_service.rb index 26971ae32..4d66c32d0 100644 --- a/app/lib/api_particulier/services/sources_service.rb +++ b/app/lib/api_particulier/services/sources_service.rb @@ -8,6 +8,7 @@ module APIParticulier def available_sources @procedure.api_particulier_scopes .map { |provider_and_scope| raw_scopes[provider_and_scope] } + .compact .map { |provider, scope| extract_sources(provider, scope) } .reduce({}) { |acc, el| acc.deep_merge(el) } end diff --git a/spec/lib/api_particulier/services/sources_service_spec.rb b/spec/lib/api_particulier/services/sources_service_spec.rb index 33e0c5208..9619732b5 100644 --- a/spec/lib/api_particulier/services/sources_service_spec.rb +++ b/spec/lib/api_particulier/services/sources_service_spec.rb @@ -31,6 +31,12 @@ describe APIParticulier::Services::SourcesService do it { is_expected.to match(cnaf_allocataires_and_enfants) } end + + context 'when a procedure has an unknown scope' do + let(:api_particulier_scopes) { ['unknown_scope'] } + + it { is_expected.to match({}) } + end end describe '#sanitize' do From cd7bafaa0d770df5e8a57acac5bd8f011aa45d70 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 1 Oct 2021 12:57:33 +0200 Subject: [PATCH 18/41] clean log_out spec helper --- spec/features/routing/full_scenario_spec.rb | 2 +- spec/support/feature_helpers.rb | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/spec/features/routing/full_scenario_spec.rb b/spec/features/routing/full_scenario_spec.rb index ffdf53704..47cdee2d5 100644 --- a/spec/features/routing/full_scenario_spec.rb +++ b/spec/features/routing/full_scenario_spec.rb @@ -63,7 +63,7 @@ feature 'The routing', js: true do # publish publish_procedure(procedure) - log_out(old_layout: true) + log_out # 2 users fill a dossier in each group user_send_dossier(scientifique_user, 'scientifique') diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index 2ecd89893..371a906e8 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -142,14 +142,10 @@ module FeatureHelpers have_css("##{form_id_for(libelle)}[value=\"#{with}\"]") end - def log_out(old_layout: false) - if old_layout - page.all('.dropdown-button').first.click - click_on 'Se déconnecter' - else - click_button(title: 'Mon compte') - click_on 'Se déconnecter' - end + def log_out + click_button(title: 'Mon compte') + click_on 'Se déconnecter' + expect(page).to have_current_path(root_path) end From 87de9e38c620762458f7f523fecc84699d587a1a Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 6 Oct 2021 12:03:12 +0200 Subject: [PATCH 19/41] allow draft to be saved with invalid cnaf champ --- app/controllers/users/dossiers_controller.rb | 14 +++++++++++++- app/models/champs/cnaf_champ.rb | 18 ++++++++++-------- .../api_particulier/api_particulier_spec.rb | 13 +++++++++++-- spec/models/champs/cnaf_champ_spec.rb | 9 ++++++++- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 31fcb4b82..721ddf54b 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -382,7 +382,8 @@ module Users if @dossier.champs.any?(&:changed_for_autosave?) @dossier.last_champ_updated_at = Time.zone.now end - if !@dossier.save + + if !@dossier.save(**validation_options) errors += @dossier.errors.full_messages elsif change_groupe_instructeur? @dossier.assign_to_groupe_instructeur(groupe_instructeur_from_params) @@ -453,5 +454,16 @@ module Users def save_draft? dossier.brouillon? && !params[:submit_draft] end + + def validation_options + if save_draft? + { context: :brouillon } + else + # rubocop:disable Lint/BooleanSymbol + # Force ActiveRecord to re-validate associated records. + { context: :false } + # rubocop:enable Lint/BooleanSymbol + end + end end end diff --git a/app/models/champs/cnaf_champ.rb b/app/models/champs/cnaf_champ.rb index 586430877..fc3c10744 100644 --- a/app/models/champs/cnaf_champ.rb +++ b/app/models/champs/cnaf_champ.rb @@ -20,8 +20,8 @@ # class Champs::CnafChamp < Champs::TextChamp # see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/cnaf-input-validation.middleware.ts - validates :numero_allocataire, format: { with: /\A\d{1,7}\z/ }, if: -> { code_postal.present? } - validates :code_postal, format: { with: /\A\w{5}\z/ }, if: -> { numero_allocataire.present? } + validates :numero_allocataire, format: { with: /\A\d{1,7}\z/ }, if: -> { code_postal.present? && validation_context != :brouillon } + validates :code_postal, format: { with: /\A\w{5}\z/ }, if: -> { numero_allocataire.present? && validation_context != :brouillon } store_accessor :value_json, :numero_allocataire, :code_postal @@ -34,12 +34,14 @@ class Champs::CnafChamp < Champs::TextChamp end def fetch_external_data - APIParticulier::CnafAdapter.new( - procedure.api_particulier_token, - numero_allocataire, - code_postal, - procedure.api_particulier_sources - ).to_params + if valid? + APIParticulier::CnafAdapter.new( + procedure.api_particulier_token, + numero_allocataire, + code_postal, + procedure.api_particulier_sources + ).to_params + end end def external_id diff --git a/spec/features/api_particulier/api_particulier_spec.rb b/spec/features/api_particulier/api_particulier_spec.rb index ce7b1cbf0..de793f453 100644 --- a/spec/features/api_particulier/api_particulier_spec.rb +++ b/spec/features/api_particulier/api_particulier_spec.rb @@ -125,14 +125,23 @@ feature 'fetch API Particulier Data', js: true do click_button('Continuer') fill_in 'Le numéro d’allocataire CAF', with: numero_allocataire + fill_in 'Le code postal', with: 'wrong_code' + + blur + expect(page).to have_css('span', text: 'Brouillon enregistré', visible: true) + + dossier = Dossier.last + expect(dossier.champs.first.code_postal).to eq('wrong_code') + + click_on 'Déposer le dossier' + expect(page).to have_content(/code postal doit posséder 5 caractères/) + fill_in 'Le code postal', with: code_postal VCR.use_cassette("api_particulier/success/composition_familiale") do perform_enqueued_jobs { click_on 'Déposer le dossier' } end - dossier = Dossier.last - visit demande_dossier_path(dossier) expect(page).to have_content(/Des données.*ont été reçues depuis la CAF/) diff --git a/spec/models/champs/cnaf_champ_spec.rb b/spec/models/champs/cnaf_champ_spec.rb index 924e9fffe..405c820a2 100644 --- a/spec/models/champs/cnaf_champ_spec.rb +++ b/spec/models/champs/cnaf_champ_spec.rb @@ -38,8 +38,9 @@ describe Champs::CnafChamp, type: :model do let(:numero_allocataire) { '1234567' } let(:code_postal) { '12345' } let(:champ) { described_class.new(dossier: create(:dossier), type_de_champ: create(:type_de_champ_cnaf)) } + let(:validation_context) { :create } - subject { champ.valid? } + subject { champ.valid?(validation_context) } before do champ.numero_allocataire = numero_allocataire @@ -82,6 +83,12 @@ describe Champs::CnafChamp, type: :model do is_expected.to be false expect(champ.errors.full_messages).to eq(["Numero allocataire doit être composé au maximum de 7 chiffres"]) end + + context 'and the validation_context is :brouillon' do + let(:validation_context) { :brouillon } + + it { is_expected.to be true } + end end context 'when code_postal is invalid' do From 50b1d4ce2892f22dd2405c94c0cf99b2a3bfe657 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Tue, 12 Oct 2021 16:16:38 +0200 Subject: [PATCH 20/41] views: make france-connect-information more compact Otherwise it takes a lot of space in the form. --- .../stylesheets/france-connect-informations.scss | 12 ++++++++++++ .../dossiers/_france_connect_informations.html.haml | 8 ++++---- 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 app/assets/stylesheets/france-connect-informations.scss diff --git a/app/assets/stylesheets/france-connect-informations.scss b/app/assets/stylesheets/france-connect-informations.scss new file mode 100644 index 000000000..739c9734f --- /dev/null +++ b/app/assets/stylesheets/france-connect-informations.scss @@ -0,0 +1,12 @@ +@import "constants"; + +.france-connect-informations.card { + width: 100%; + padding-top: $default-spacer; + padding-bottom: $default-spacer; +} + +.france-connect-informations-logo img { + width: 100px; + margin-right: $default-padding; +} diff --git a/app/views/shared/dossiers/_france_connect_informations.html.haml b/app/views/shared/dossiers/_france_connect_informations.html.haml index b875f26b4..bd73c94e9 100644 --- a/app/views/shared/dossiers/_france_connect_informations.html.haml +++ b/app/views/shared/dossiers/_france_connect_informations.html.haml @@ -1,7 +1,7 @@ -.card.featured - .flex.justify-center - = image_tag "logo-france-connect.png", alt: "France Connect logo", width: 200, class: "mb-2" - .card-title +.france-connect-informations.card.featured.flex.align-center + .france-connect-informations-logo + = image_tag "logo-france-connect.png", alt: "France Connect logo" + %div - if user_information.updated_at.present? Le dossier a été déposé par le compte de #{user_information&.given_name} #{user_information&.family_name}, authentifié par France Connect le #{user_information.updated_at.strftime('%d/%m/%Y')}. - else From 9be16a1208bc6afc34829dbf664c41c6f5a91512 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Tue, 12 Oct 2021 16:17:01 +0200 Subject: [PATCH 21/41] views: rename the procedure_publish_label helper The new text makes more sense. --- app/helpers/procedure_helper.rb | 2 +- .../new_administrateur/procedures/_publication_form.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/procedure_helper.rb b/app/helpers/procedure_helper.rb index f8ffca559..4d64efe0b 100644 --- a/app/helpers/procedure_helper.rb +++ b/app/helpers/procedure_helper.rb @@ -13,7 +13,7 @@ module ProcedureHelper safe_join(parts, ' ') end - def procedure_publish_text(procedure, key) + def procedure_publish_label(procedure, key) # i18n-tasks-use t('modal.publish.body.publish') # i18n-tasks-use t('modal.publish.body.reopen') # i18n-tasks-use t('modal.publish.submit.publish') diff --git a/app/views/new_administrateur/procedures/_publication_form.html.haml b/app/views/new_administrateur/procedures/_publication_form.html.haml index 1af036c6a..0989ab17b 100644 --- a/app/views/new_administrateur/procedures/_publication_form.html.haml +++ b/app/views/new_administrateur/procedures/_publication_form.html.haml @@ -40,4 +40,4 @@ - if errors.details[:path].present? - options[:disabled] = :disabled .flex.justify-end - = submit_tag procedure_publish_text(procedure, :submit), options + = submit_tag procedure_publish_label(procedure, :submit), options From f9003872e71b14e3183464d27ea555871b0e9109 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Oct 2021 18:15:29 +0000 Subject: [PATCH 22/41] build(deps): bump puma from 5.3.1 to 5.5.1 Bumps [puma](https://github.com/puma/puma) from 5.3.1 to 5.5.1. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v5.3.1...v5.5.1) --- updated-dependencies: - dependency-name: puma dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a76117296..bc5731471 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -490,7 +490,7 @@ GEM byebug (~> 11.0) pry (~> 0.13.0) public_suffix (4.0.6) - puma (5.3.1) + puma (5.5.1) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) From 288ea82286f261b2d29e0d6eddd556b26662245d Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 7 Oct 2021 15:43:25 +0200 Subject: [PATCH 23/41] fix(avis): add foreign key to avis dossier_id --- .../20211012100819_add_foreign_key_to_avis_dossier_id.rb | 5 +++++ db/schema.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20211012100819_add_foreign_key_to_avis_dossier_id.rb diff --git a/db/migrate/20211012100819_add_foreign_key_to_avis_dossier_id.rb b/db/migrate/20211012100819_add_foreign_key_to_avis_dossier_id.rb new file mode 100644 index 000000000..aab456696 --- /dev/null +++ b/db/migrate/20211012100819_add_foreign_key_to_avis_dossier_id.rb @@ -0,0 +1,5 @@ +class AddForeignKeyToAvisDossierId < ActiveRecord::Migration[6.1] + def change + add_foreign_key :avis, :dossiers + end +end diff --git a/db/schema.rb b/db/schema.rb index cd272f562..2f19186a9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_10_06_164955) do +ActiveRecord::Schema.define(version: 2021_10_12_100819) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -814,6 +814,7 @@ ActiveRecord::Schema.define(version: 2021_10_06_164955) do add_foreign_key "assign_tos", "groupe_instructeurs" add_foreign_key "attestation_templates", "procedures" add_foreign_key "attestations", "dossiers" + add_foreign_key "avis", "dossiers" add_foreign_key "avis", "experts_procedures" add_foreign_key "bulk_messages_groupe_instructeurs", "bulk_messages" add_foreign_key "bulk_messages_groupe_instructeurs", "groupe_instructeurs" From 4caf2f9592577db867a9c1f31cdf1ddb793ae752 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 13 Oct 2021 13:55:09 +0200 Subject: [PATCH 24/41] fix(champs): remove redundant description --- app/views/shared/dossiers/editable_champs/_engagement.html.haml | 2 -- app/views/shared/dossiers/editable_champs/_yes_no.html.haml | 2 -- 2 files changed, 4 deletions(-) diff --git a/app/views/shared/dossiers/editable_champs/_engagement.html.haml b/app/views/shared/dossiers/editable_champs/_engagement.html.haml index 5bd197a73..707b2adcd 100644 --- a/app/views/shared/dossiers/editable_champs/_engagement.html.haml +++ b/app/views/shared/dossiers/editable_champs/_engagement.html.haml @@ -2,5 +2,3 @@ { required: champ.mandatory? }, 'on', 'off' - -%br diff --git a/app/views/shared/dossiers/editable_champs/_yes_no.html.haml b/app/views/shared/dossiers/editable_champs/_yes_no.html.haml index 11cd71ad0..6a6099dda 100644 --- a/app/views/shared/dossiers/editable_champs/_yes_no.html.haml +++ b/app/views/shared/dossiers/editable_champs/_yes_no.html.haml @@ -1,6 +1,4 @@ %fieldset.radios - %legend.mandatory-explanation - Sélectionnez une des deux valeurs %label = form.radio_button :value, true Oui From 2f470b25aa3fdba7b07793119e7d827a8dcf9ba5 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 6 Oct 2021 16:30:51 +0200 Subject: [PATCH 25/41] spec cleaning --- .../particulier_controller_spec.rb | 1 - .../france_connect_particulier_spec.rb | 38 +++---------------- 2 files changed, 6 insertions(+), 33 deletions(-) diff --git a/spec/controllers/france_connect/particulier_controller_spec.rb b/spec/controllers/france_connect/particulier_controller_spec.rb index 9a11c282e..5cce7c467 100644 --- a/spec/controllers/france_connect/particulier_controller_spec.rb +++ b/spec/controllers/france_connect/particulier_controller_spec.rb @@ -79,7 +79,6 @@ describe FranceConnect::ParticulierController, type: :controller do context 'when france_connect_particulier_id does not exist in database' do it { expect { subject }.to change { FranceConnectInformation.count }.by(1) } - it { expect { subject }.to change { FranceConnectInformation.count }.by(1) } describe 'FranceConnectInformation attributs' do let(:stored_fci) { FranceConnectInformation.last } diff --git a/spec/features/france_connect/france_connect_particulier_spec.rb b/spec/features/france_connect/france_connect_particulier_spec.rb index 7dbbbef1f..6c5b98520 100644 --- a/spec/features/france_connect/france_connect_particulier_spec.rb +++ b/spec/features/france_connect/france_connect_particulier_spec.rb @@ -21,17 +21,13 @@ feature 'France Connect Particulier Connexion' do end context 'when user is on login page' do - before do - visit new_user_session_path - end + before { visit new_user_session_path } scenario 'link to France Connect is present' do expect(page).to have_css('.france-connect-login-button') end context 'and click on france connect link' do - let(:code) { 'plop' } - context 'when authentification is ok' do before do allow_any_instance_of(FranceConnectParticulierClient).to receive(:authorization_uri).and_return(france_connect_particulier_callback_path(code: code)) @@ -39,44 +35,22 @@ feature 'France Connect Particulier Connexion' do end context 'when is the first connexion' do - let(:france_connect_information) do - build(:france_connect_information, - france_connect_particulier_id: france_connect_particulier_id, - given_name: given_name, - family_name: family_name, - birthdate: birthdate, - birthplace: birthplace, - gender: gender, - email_france_connect: email) - end + let(:france_connect_information) { build(:france_connect_information, user_info) } - before do - page.find('.france-connect-login-button').click - end + before { page.find('.france-connect-login-button').click } scenario 'he is redirected to user dossiers page' do expect(page).to have_content('Dossiers') + expect(User.find_by(email: email)).not_to be nil end end context 'when is not the first connexion' do let!(:france_connect_information) do - create(:france_connect_information, - :with_user, - france_connect_particulier_id: france_connect_particulier_id, - given_name: given_name, - family_name: family_name, - birthdate: birthdate, - birthplace: birthplace, - gender: gender, - email_france_connect: email, - created_at: Time.zone.parse('12/12/2012'), - updated_at: Time.zone.parse('12/12/2012')) + create(:france_connect_information, :with_user, user_info.merge(created_at: Time.zone.parse('12/12/2012'), updated_at: Time.zone.parse('12/12/2012'))) end - before do - page.find('.france-connect-login-button').click - end + before { page.find('.france-connect-login-button').click } scenario 'he is redirected to user dossiers page' do expect(page).to have_content('Dossiers') From 5aaf46258a979229e1e261d77706ead28c0913bf Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 11 Oct 2021 11:28:47 +0200 Subject: [PATCH 26/41] remove obsolete devise scope --- app/controllers/france_connect/particulier_controller.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/controllers/france_connect/particulier_controller.rb b/app/controllers/france_connect/particulier_controller.rb index 595a50e78..fbf46e11f 100644 --- a/app/controllers/france_connect/particulier_controller.rb +++ b/app/controllers/france_connect/particulier_controller.rb @@ -39,14 +39,6 @@ class FranceConnect::ParticulierController < ApplicationController sign_out :user end - if instructeur_signed_in? - sign_out :instructeur - end - - if administrateur_signed_in? - sign_out :administrateur - end - sign_in user user.update_attribute('loged_in_with_france_connect', User.loged_in_with_france_connects.fetch(:particulier)) From 06dee2e0233f4270a4bac86a5a37cbd8567885a4 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 11 Oct 2021 11:30:45 +0200 Subject: [PATCH 27/41] refactor controller to avoid return --- app/controllers/france_connect/particulier_controller.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/controllers/france_connect/particulier_controller.rb b/app/controllers/france_connect/particulier_controller.rb index fbf46e11f..449153961 100644 --- a/app/controllers/france_connect/particulier_controller.rb +++ b/app/controllers/france_connect/particulier_controller.rb @@ -13,14 +13,15 @@ class FranceConnect::ParticulierController < ApplicationController fci = FranceConnectService.find_or_retrieve_france_connect_information(params[:code]) fci.associate_user! - if fci.user && !fci.user.can_france_connect? + user = fci.user + + if user.can_france_connect? + connect_france_connect_particulier(user) + else fci.destroy redirect_to new_user_session_path, alert: t('errors.messages.france_connect.forbidden_html', reset_link: new_user_password_path) - return end - connect_france_connect_particulier(fci.user) - rescue Rack::OAuth2::Client::Error => e Rails.logger.error e.message redirect_france_connect_error_connection From 6826bf03b0ebe92ce735c36ecb584c835c82d813 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 11 Oct 2021 11:39:14 +0200 Subject: [PATCH 28/41] Sign in with a user linked by france connect sub (openid) instead of looking linked user by email because : - follows FC recommendation to fetch ds account by openid - the email is not a valid key as many user can share the same FCI email. The following scenario is now working A user A (email: 1@mail.com) uses FC to connect to DS => It is connected as 1@mail.com Another user B (email: generic@mail.com) uses FC to connect => It is connected as generic@mail.com The first user A change its FC email to generic@mail.com and connect to DS => It is still connected as 1@mail.com --- .../france_connect/particulier_controller.rb | 5 ++- app/models/france_connect_information.rb | 26 +++++------ .../particulier_controller_spec.rb | 44 +++++++++++-------- .../models/france_connect_information_spec.rb | 10 ----- 4 files changed, 40 insertions(+), 45 deletions(-) diff --git a/app/controllers/france_connect/particulier_controller.rb b/app/controllers/france_connect/particulier_controller.rb index 449153961..bcfa9b577 100644 --- a/app/controllers/france_connect/particulier_controller.rb +++ b/app/controllers/france_connect/particulier_controller.rb @@ -11,7 +11,10 @@ class FranceConnect::ParticulierController < ApplicationController def callback fci = FranceConnectService.find_or_retrieve_france_connect_information(params[:code]) - fci.associate_user! + + if fci.user.nil? + fci.associate_user! + end user = fci.user diff --git a/app/models/france_connect_information.rb b/app/models/france_connect_information.rb index d76ee60d8..1bd3220c1 100644 --- a/app/models/france_connect_information.rb +++ b/app/models/france_connect_information.rb @@ -21,21 +21,17 @@ class FranceConnectInformation < ApplicationRecord validates :france_connect_particulier_id, presence: true, allow_blank: false, allow_nil: false def associate_user! - user = User.find_by(email: email_france_connect.downcase) - - if user.nil? - begin - user = User.create!( - email: email_france_connect.downcase, - password: Devise.friendly_token[0, 20], - confirmed_at: Time.zone.now - ) - rescue ActiveRecord::RecordNotUnique - # ignore this exception because we check before is user is nil. - # exception can be raised in race conditions, when FranceConnect calls callback 2 times. - # At the 2nd call, user is nil but exception is raised at the creation of the user - # because the first call has already created a user - end + begin + user = User.create!( + email: email_france_connect.downcase, + password: Devise.friendly_token[0, 20], + confirmed_at: Time.zone.now + ) + rescue ActiveRecord::RecordNotUnique + # ignore this exception because we check before is user is nil. + # exception can be raised in race conditions, when FranceConnect calls callback 2 times. + # At the 2nd call, user is nil but exception is raised at the creation of the user + # because the first call has already created a user end update_attribute('user_id', user.id) diff --git a/spec/controllers/france_connect/particulier_controller_spec.rb b/spec/controllers/france_connect/particulier_controller_spec.rb index 5cce7c467..569d444e3 100644 --- a/spec/controllers/france_connect/particulier_controller_spec.rb +++ b/spec/controllers/france_connect/particulier_controller_spec.rb @@ -1,6 +1,6 @@ describe FranceConnect::ParticulierController, type: :controller do let(:birthdate) { '20150821' } - let(:email) { 'test@test.com' } + let(:email) { 'email_from_fc@test.com' } let(:user_info) do { @@ -49,31 +49,37 @@ describe FranceConnect::ParticulierController, type: :controller do .and_return(FranceConnectInformation.new(user_info)) end - context 'when france_connect_particulier_id exist in database' do - let!(:france_connect_information) { create(:france_connect_information, :with_user, user_info) } - let(:user) { france_connect_information.user } + context 'when france_connect_particulier_id exists in database' do + let!(:fci) { FranceConnectInformation.create!(user_info.merge(user_id: fc_user.id)) } - it { expect { subject }.not_to change { FranceConnectInformation.count } } + context 'and is linked to an user' do + let(:fc_user) { create(:user, email: 'associated_user@a.com') } - it do - subject - expect(user.reload.loged_in_with_france_connect).to eq(User.loged_in_with_france_connects.fetch(:particulier)) + it { expect { subject }.not_to change { FranceConnectInformation.count } } + + it 'signs in with the fci associated user' do + subject + expect(controller.current_user).to eq(fc_user) + expect(fc_user.reload.loged_in_with_france_connect).to eq(User.loged_in_with_france_connects.fetch(:particulier)) + end + + context 'and the user has a stored location' do + let(:stored_location) { '/plip/plop' } + before { controller.store_location_for(:user, stored_location) } + + it { is_expected.to redirect_to(stored_location) } + end end - context 'and the user has a stored location' do - let(:stored_location) { '/plip/plop' } - before { controller.store_location_for(:user, stored_location) } + context 'and is linked an instructeur' do + let(:fc_user) { create(:instructeur, email: 'another_email@a.com').user } - it { is_expected.to redirect_to(stored_location) } - end - - context 'and the user is also instructeur' do - let!(:instructeur) { create(:instructeur, email: email) } before { subject } - it { expect(response).to redirect_to(new_user_session_path) } - - it { expect(flash[:alert]).to be_present } + it do + expect(response).to redirect_to(new_user_session_path) + expect(flash[:alert]).to be_present + end end end diff --git a/spec/models/france_connect_information_spec.rb b/spec/models/france_connect_information_spec.rb index 6e8a0822b..150b92fc3 100644 --- a/spec/models/france_connect_information_spec.rb +++ b/spec/models/france_connect_information_spec.rb @@ -18,15 +18,5 @@ describe FranceConnectInformation, type: :model do expect(fci.user.email).to eq(fci.email_france_connect) end end - - context 'when a user with same email (but who is not an instructeur) exist' do - let(:user) { create(:user) } - let(:fci) { build(:france_connect_information, email_france_connect: user.email) } - let(:subject) { fci.associate_user! } - - before { subject } - - it { expect(fci.user).to eq(user) } - end end end From 6e6635560f2402a327048668b1a4115154e2c27b Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 11 Oct 2021 12:32:33 +0200 Subject: [PATCH 29/41] Add merge token to FCI --- app/models/france_connect_information.rb | 2 ++ ...957_add_token_columns_to_france_connect_information.rb | 8 ++++++++ db/schema.rb | 3 +++ 3 files changed, 13 insertions(+) create mode 100644 db/migrate/20211011102957_add_token_columns_to_france_connect_information.rb diff --git a/app/models/france_connect_information.rb b/app/models/france_connect_information.rb index 1bd3220c1..06d258fc6 100644 --- a/app/models/france_connect_information.rb +++ b/app/models/france_connect_information.rb @@ -10,6 +10,8 @@ # family_name :string # gender :string # given_name :string +# merge_token :string +# merge_token_created_at :datetime # created_at :datetime not null # updated_at :datetime not null # france_connect_particulier_id :string diff --git a/db/migrate/20211011102957_add_token_columns_to_france_connect_information.rb b/db/migrate/20211011102957_add_token_columns_to_france_connect_information.rb new file mode 100644 index 000000000..1a8b9ee73 --- /dev/null +++ b/db/migrate/20211011102957_add_token_columns_to_france_connect_information.rb @@ -0,0 +1,8 @@ +class AddTokenColumnsToFranceConnectInformation < ActiveRecord::Migration[6.1] + def change + add_column :france_connect_informations, :merge_token, :string + add_column :france_connect_informations, :merge_token_created_at, :datetime + + add_index :france_connect_informations, :merge_token + end +end diff --git a/db/schema.rb b/db/schema.rb index 2f19186a9..7db51be64 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -478,6 +478,9 @@ ActiveRecord::Schema.define(version: 2021_10_12_100819) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.jsonb "data" + t.string "merge_token" + t.datetime "merge_token_created_at" + t.index ["merge_token"], name: "index_france_connect_informations_on_merge_token" t.index ["user_id"], name: "index_france_connect_informations_on_user_id" end From 461b7741887009f28ab1f9c12ab7370bde6226f4 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 12 Oct 2021 23:32:30 +0200 Subject: [PATCH 30/41] a password input must not be that wide --- app/assets/stylesheets/forms.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 717b9b51e..c815ac74d 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -241,6 +241,7 @@ } input[type=email], + input[type=password], input[type=number], input[type=tel], { max-width: 500px; From 2e118a8f5b4cd906c5f34fe365df8621cfbd79d8 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 12 Oct 2021 23:53:11 +0200 Subject: [PATCH 31/41] allow unattached fci --- app/models/france_connect_information.rb | 2 +- ...r_id_not_null_constraint_to_france_connect_information.rb | 5 +++++ db/schema.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20211011083203_remove_user_id_not_null_constraint_to_france_connect_information.rb diff --git a/app/models/france_connect_information.rb b/app/models/france_connect_information.rb index 06d258fc6..c4deb17a2 100644 --- a/app/models/france_connect_information.rb +++ b/app/models/france_connect_information.rb @@ -15,7 +15,7 @@ # created_at :datetime not null # updated_at :datetime not null # france_connect_particulier_id :string -# user_id :integer not null +# user_id :integer # class FranceConnectInformation < ApplicationRecord belongs_to :user, optional: true diff --git a/db/migrate/20211011083203_remove_user_id_not_null_constraint_to_france_connect_information.rb b/db/migrate/20211011083203_remove_user_id_not_null_constraint_to_france_connect_information.rb new file mode 100644 index 000000000..212352604 --- /dev/null +++ b/db/migrate/20211011083203_remove_user_id_not_null_constraint_to_france_connect_information.rb @@ -0,0 +1,5 @@ +class RemoveUserIdNotNullConstraintToFranceConnectInformation < ActiveRecord::Migration[6.1] + def change + change_column_null(:france_connect_informations, :user_id, true) + end +end diff --git a/db/schema.rb b/db/schema.rb index 7db51be64..219a263a8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -473,7 +473,7 @@ ActiveRecord::Schema.define(version: 2021_10_12_100819) do t.date "birthdate" t.string "birthplace" t.string "france_connect_particulier_id" - t.integer "user_id", null: false + t.integer "user_id" t.string "email_france_connect" t.datetime "created_at", null: false t.datetime "updated_at", null: false From 34862f41e08f5054b0b35df2c1c334c3a16981c6 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 13 Oct 2021 00:14:12 +0200 Subject: [PATCH 32/41] Add fci valid_for_merge --- app/models/france_connect_information.rb | 6 +++++ .../models/france_connect_information_spec.rb | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/app/models/france_connect_information.rb b/app/models/france_connect_information.rb index c4deb17a2..bdc6d5d5b 100644 --- a/app/models/france_connect_information.rb +++ b/app/models/france_connect_information.rb @@ -18,6 +18,8 @@ # user_id :integer # class FranceConnectInformation < ApplicationRecord + MERGE_VALIDITY = 15.minutes + belongs_to :user, optional: true validates :france_connect_particulier_id, presence: true, allow_blank: false, allow_nil: false @@ -39,4 +41,8 @@ class FranceConnectInformation < ApplicationRecord update_attribute('user_id', user.id) touch # needed to update updated_at column end + + def valid_for_merge? + (MERGE_VALIDITY.ago < merge_token_created_at) && user_id.nil? + end end diff --git a/spec/models/france_connect_information_spec.rb b/spec/models/france_connect_information_spec.rb index 150b92fc3..ab321cec5 100644 --- a/spec/models/france_connect_information_spec.rb +++ b/spec/models/france_connect_information_spec.rb @@ -19,4 +19,28 @@ describe FranceConnectInformation, type: :model do end end end + + describe '#valid_for_merge?' do + let(:fci) { create(:france_connect_information) } + + subject { fci.valid_for_merge? } + + context 'when the merge token is young enough' do + before { fci.merge_token_created_at = 1.minute.ago } + + it { is_expected.to be(true) } + + context 'but the fci is already linked to an user' do + before { fci.update(user: create(:user)) } + + it { is_expected.to be(false) } + end + end + + context 'when the merge token is too old' do + before { fci.merge_token_created_at = (FranceConnectInformation::MERGE_VALIDITY + 1.minute).ago } + + it { is_expected.to be(false) } + end + end end From 09f828a6a217629d2b06b3e7a484d65e40b9ac79 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 13 Oct 2021 00:40:15 +0200 Subject: [PATCH 33/41] create_merge_token! --- app/models/france_connect_information.rb | 7 +++++++ spec/models/france_connect_information_spec.rb | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/app/models/france_connect_information.rb b/app/models/france_connect_information.rb index bdc6d5d5b..6f1d57c4a 100644 --- a/app/models/france_connect_information.rb +++ b/app/models/france_connect_information.rb @@ -42,6 +42,13 @@ class FranceConnectInformation < ApplicationRecord touch # needed to update updated_at column end + def create_merge_token! + merge_token = SecureRandom.uuid + update(merge_token: merge_token, merge_token_created_at: Time.zone.now) + + merge_token + end + def valid_for_merge? (MERGE_VALIDITY.ago < merge_token_created_at) && user_id.nil? end diff --git a/spec/models/france_connect_information_spec.rb b/spec/models/france_connect_information_spec.rb index ab321cec5..e005a0094 100644 --- a/spec/models/france_connect_information_spec.rb +++ b/spec/models/france_connect_information_spec.rb @@ -43,4 +43,15 @@ describe FranceConnectInformation, type: :model do it { is_expected.to be(false) } end end + + describe '#create_merge_token!' do + let(:fci) { create(:france_connect_information) } + + it 'returns a merge_token and register it s creation date' do + token = fci.create_merge_token! + + expect(fci.merge_token).to eq(token) + expect(fci.merge_token_created_at).not_to be_nil + end + end end From f6879eba607df58d47777f6fa965b01a145fde39 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 13 Oct 2021 00:18:00 +0200 Subject: [PATCH 34/41] associate_user take a target email --- app/controllers/france_connect/particulier_controller.rb | 2 +- app/models/france_connect_information.rb | 4 ++-- spec/models/france_connect_information_spec.rb | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/controllers/france_connect/particulier_controller.rb b/app/controllers/france_connect/particulier_controller.rb index bcfa9b577..8a2a7139f 100644 --- a/app/controllers/france_connect/particulier_controller.rb +++ b/app/controllers/france_connect/particulier_controller.rb @@ -13,7 +13,7 @@ class FranceConnect::ParticulierController < ApplicationController fci = FranceConnectService.find_or_retrieve_france_connect_information(params[:code]) if fci.user.nil? - fci.associate_user! + fci.associate_user!(fci.email_france_connect) end user = fci.user diff --git a/app/models/france_connect_information.rb b/app/models/france_connect_information.rb index 6f1d57c4a..49f6fd7d9 100644 --- a/app/models/france_connect_information.rb +++ b/app/models/france_connect_information.rb @@ -24,10 +24,10 @@ class FranceConnectInformation < ApplicationRecord validates :france_connect_particulier_id, presence: true, allow_blank: false, allow_nil: false - def associate_user! + def associate_user!(email) begin user = User.create!( - email: email_france_connect.downcase, + email: email.downcase, password: Devise.friendly_token[0, 20], confirmed_at: Time.zone.now ) diff --git a/spec/models/france_connect_information_spec.rb b/spec/models/france_connect_information_spec.rb index e005a0094..621c729b3 100644 --- a/spec/models/france_connect_information_spec.rb +++ b/spec/models/france_connect_information_spec.rb @@ -9,13 +9,16 @@ describe FranceConnectInformation, type: :model do describe 'associate_user!' do context 'when there is no user with same email' do + let(:email) { 'A@email.com' } let(:fci) { build(:france_connect_information) } - let(:subject) { fci.associate_user! } + + subject { fci.associate_user!(email) } it { expect { subject }.to change(User, :count).by(1) } + it do subject - expect(fci.user.email).to eq(fci.email_france_connect) + expect(fci.user.email).to eq('a@email.com') end end end From f7299da1e75a70608c3f31830ee21de35c536b25 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 13 Oct 2021 00:45:20 +0200 Subject: [PATCH 35/41] launch merge process if an unlinked DS account with the same email exists --- .../france_connect/particulier_controller.rb | 26 +++++++++++----- .../particulier/merge.html.haml | 5 ++++ config/routes.rb | 1 + .../particulier_controller_spec.rb | 30 +++++++++++++++++-- 4 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 app/views/france_connect/particulier/merge.html.haml diff --git a/app/controllers/france_connect/particulier_controller.rb b/app/controllers/france_connect/particulier_controller.rb index 8a2a7139f..3efe18e97 100644 --- a/app/controllers/france_connect/particulier_controller.rb +++ b/app/controllers/france_connect/particulier_controller.rb @@ -13,16 +13,23 @@ class FranceConnect::ParticulierController < ApplicationController fci = FranceConnectService.find_or_retrieve_france_connect_information(params[:code]) if fci.user.nil? - fci.associate_user!(fci.email_france_connect) - end + preexisting_unlinked_user = User.find_by(email: fci.email_france_connect.downcase) - user = fci.user - - if user.can_france_connect? - connect_france_connect_particulier(user) + if preexisting_unlinked_user.nil? + fci.associate_user!(fci.email_france_connect) + connect_france_connect_particulier(fci.user) + else + redirect_to france_connect_particulier_merge_path(fci.create_merge_token!) + end else - fci.destroy - redirect_to new_user_session_path, alert: t('errors.messages.france_connect.forbidden_html', reset_link: new_user_password_path) + user = fci.user + + if user.can_france_connect? + connect_france_connect_particulier(user) + else + fci.destroy + redirect_to new_user_session_path, alert: t('errors.messages.france_connect.forbidden_html', reset_link: new_user_password_path) + end end rescue Rack::OAuth2::Client::Error => e @@ -30,6 +37,9 @@ class FranceConnect::ParticulierController < ApplicationController redirect_france_connect_error_connection end + def merge + end + private def redirect_to_login_if_fc_aborted diff --git a/app/views/france_connect/particulier/merge.html.haml b/app/views/france_connect/particulier/merge.html.haml new file mode 100644 index 000000000..ad4a0489c --- /dev/null +++ b/app/views/france_connect/particulier/merge.html.haml @@ -0,0 +1,5 @@ += content_for :title, "Fusion des comptes FC et #{APPLICATION_NAME}" + +.container + %h1.page-title Fusion des comptes FranceConnect et #{APPLICATION_NAME} + diff --git a/config/routes.rb b/config/routes.rb index 3e8b1168b..3560cc97a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -124,6 +124,7 @@ Rails.application.routes.draw do namespace :france_connect do get 'particulier' => 'particulier#login' get 'particulier/callback' => 'particulier#callback' + get 'particulier/merge/:merge_token' => 'particulier#merge', as: :particulier_merge end namespace :champs do diff --git a/spec/controllers/france_connect/particulier_controller_spec.rb b/spec/controllers/france_connect/particulier_controller_spec.rb index 569d444e3..3a416491d 100644 --- a/spec/controllers/france_connect/particulier_controller_spec.rb +++ b/spec/controllers/france_connect/particulier_controller_spec.rb @@ -1,6 +1,6 @@ describe FranceConnect::ParticulierController, type: :controller do let(:birthdate) { '20150821' } - let(:email) { 'email_from_fc@test.com' } + let(:email) { 'EMAIL_from_fc@test.com' } let(:user_info) do { @@ -50,7 +50,7 @@ describe FranceConnect::ParticulierController, type: :controller do end context 'when france_connect_particulier_id exists in database' do - let!(:fci) { FranceConnectInformation.create!(user_info.merge(user_id: fc_user.id)) } + let!(:fci) { FranceConnectInformation.create!(user_info.merge(user_id: fc_user&.id)) } context 'and is linked to an user' do let(:fc_user) { create(:user, email: 'associated_user@a.com') } @@ -81,6 +81,32 @@ describe FranceConnect::ParticulierController, type: :controller do expect(flash[:alert]).to be_present end end + + context 'and is not linked to an user' do + let(:fc_user) { nil } + + context 'and no user with the same email exists' do + it 'creates an user with the same email and log in' do + expect { subject }.to change { User.count }.by(1) + + user = User.last + + expect(user.email).to eq(email.downcase) + expect(controller.current_user).to eq(user) + expect(response).to redirect_to(root_path) + end + end + + context 'and an user with the same email exists' do + let!(:preexisting_user) { create(:user, email: email) } + + it 'redirects to the merge process' do + expect { subject }.not_to change { User.count } + + expect(response).to redirect_to(france_connect_particulier_merge_path(fci.reload.merge_token)) + end + end + end end context 'when france_connect_particulier_id does not exist in database' do From 218e4633a9f165b7b71bceff1e5217d5cf04e433 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 13 Oct 2021 09:23:14 +0200 Subject: [PATCH 36/41] securely retrieve fci --- .../france_connect/particulier_controller.rb | 15 +++++++++ .../particulier/merge.html.haml | 7 ++++ .../particulier_controller_spec.rb | 32 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/app/controllers/france_connect/particulier_controller.rb b/app/controllers/france_connect/particulier_controller.rb index 3efe18e97..ac99880bb 100644 --- a/app/controllers/france_connect/particulier_controller.rb +++ b/app/controllers/france_connect/particulier_controller.rb @@ -1,5 +1,6 @@ class FranceConnect::ParticulierController < ApplicationController before_action :redirect_to_login_if_fc_aborted, only: [:callback] + before_action :securely_retrieve_fci, only: [:merge] def login if FranceConnectService.enabled? @@ -42,6 +43,16 @@ class FranceConnect::ParticulierController < ApplicationController private + def securely_retrieve_fci + @fci = FranceConnectInformation.find_by(merge_token: merge_token_params) + + if @fci.nil? || !@fci.valid_for_merge? + flash.alert = 'Votre compte FranceConnect a expiré, veuillez recommencer.' + + redirect_to root_path + end + end + def redirect_to_login_if_fc_aborted if params[:code].blank? redirect_to new_user_session_path @@ -64,4 +75,8 @@ class FranceConnect::ParticulierController < ApplicationController flash.alert = t('errors.messages.france_connect.connexion') redirect_to(new_user_session_path) end + + def merge_token_params + params[:merge_token] + end end diff --git a/app/views/france_connect/particulier/merge.html.haml b/app/views/france_connect/particulier/merge.html.haml index ad4a0489c..6ec910cdb 100644 --- a/app/views/france_connect/particulier/merge.html.haml +++ b/app/views/france_connect/particulier/merge.html.haml @@ -3,3 +3,10 @@ .container %h1.page-title Fusion des comptes FranceConnect et #{APPLICATION_NAME} + %p + Bonjour, + %br + %br + Votre compte FranceConnect utilise #{@fci.email_france_connect} comme email de contact. + %br + Or il existe un compte sur #{APPLICATION_NAME} avec cet email. diff --git a/spec/controllers/france_connect/particulier_controller_spec.rb b/spec/controllers/france_connect/particulier_controller_spec.rb index 3a416491d..a67eadbac 100644 --- a/spec/controllers/france_connect/particulier_controller_spec.rb +++ b/spec/controllers/france_connect/particulier_controller_spec.rb @@ -135,4 +135,36 @@ describe FranceConnect::ParticulierController, type: :controller do it { expect(flash[:alert]).to be_present } end end + + describe '#merge' do + let(:fci) { FranceConnectInformation.create!(user_info) } + let(:merge_token) { fci.create_merge_token! } + + subject { get :merge, params: { merge_token: merge_token } } + + context 'when the merge token is valid' do + it { expect(subject).to have_http_status(:ok) } + end + + context 'when the merge token is invalid' do + before do + merge_token + fci.update(merge_token_created_at: 2.years.ago) + end + + it do + expect(subject).to redirect_to root_path + expect(flash.alert).to eq('Votre compte FranceConnect a expiré, veuillez recommencer.') + end + end + + context 'when the merge token does not exist' do + let(:merge_token) { 'i do not exist' } + + it do + expect(subject).to redirect_to root_path + expect(flash.alert).to eq('Votre compte FranceConnect a expiré, veuillez recommencer.') + end + end + end end From 19f81b594b8473881090428254c3b4fbcc3cd837 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 13 Oct 2021 09:23:40 +0200 Subject: [PATCH 37/41] merge with an existing account by using the password --- app/controllers/application_controller.rb | 4 + .../france_connect/particulier_controller.rb | 44 +++++++++- app/javascript/new_design/fc-fusion.js | 5 ++ app/javascript/packs/application.js | 4 + app/models/france_connect_information.rb | 4 + .../particulier/merge.html.haml | 21 +++++ config/routes.rb | 1 + .../particulier_controller_spec.rb | 84 ++++++++++++++++--- 8 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 app/javascript/new_design/fc-fusion.js diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9553e307f..072c3f483 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -85,6 +85,10 @@ class ApplicationController < ActionController::Base end end + def ajax_redirect(path) + "window.location.href='#{path}'" + end + protected def feature_enabled?(feature_name) diff --git a/app/controllers/france_connect/particulier_controller.rb b/app/controllers/france_connect/particulier_controller.rb index ac99880bb..e856a84fa 100644 --- a/app/controllers/france_connect/particulier_controller.rb +++ b/app/controllers/france_connect/particulier_controller.rb @@ -1,6 +1,6 @@ class FranceConnect::ParticulierController < ApplicationController before_action :redirect_to_login_if_fc_aborted, only: [:callback] - before_action :securely_retrieve_fci, only: [:merge] + before_action :securely_retrieve_fci, only: [:merge, :merge_with_existing_account] def login if FranceConnectService.enabled? @@ -41,6 +41,28 @@ class FranceConnect::ParticulierController < ApplicationController def merge end + def merge_with_existing_account + user = User.find_by(email: sanitized_email_params) + + if user.valid_for_authentication? { user.valid_password?(password_params) } + if !user.can_france_connect? + flash.alert = "#{user.email} ne peut utiliser FranceConnect" + + render js: ajax_redirect(root_path) + else + @fci.update(user: user) + @fci.delete_merge_token! + + flash.notice = "Les comptes FranceConnect et #{APPLICATION_NAME} sont à présent fusionnés" + connect_france_connect_particulier(user) + end + else + flash.alert = 'Mauvais mot de passe' + + render js: helpers.render_flash + end + end + private def securely_retrieve_fci @@ -49,7 +71,10 @@ class FranceConnect::ParticulierController < ApplicationController if @fci.nil? || !@fci.valid_for_merge? flash.alert = 'Votre compte FranceConnect a expiré, veuillez recommencer.' - redirect_to root_path + respond_to do |format| + format.html { redirect_to root_path } + format.js { render js: ajax_redirect(root_path) } + end end end @@ -68,7 +93,12 @@ class FranceConnect::ParticulierController < ApplicationController user.update_attribute('loged_in_with_france_connect', User.loged_in_with_france_connects.fetch(:particulier)) - redirect_to stored_location_for(current_user) || root_path(current_user) + redirection_location = stored_location_for(current_user) || root_path(current_user) + + respond_to do |format| + format.html { redirect_to redirection_location } + format.js { render js: ajax_redirect(root_path) } + end end def redirect_france_connect_error_connection @@ -79,4 +109,12 @@ class FranceConnect::ParticulierController < ApplicationController def merge_token_params params[:merge_token] end + + def password_params + params[:password] + end + + def sanitized_email_params + params[:email]&.gsub(/[[:space:]]/, ' ')&.strip&.downcase + end end diff --git a/app/javascript/new_design/fc-fusion.js b/app/javascript/new_design/fc-fusion.js new file mode 100644 index 000000000..bb1c904c3 --- /dev/null +++ b/app/javascript/new_design/fc-fusion.js @@ -0,0 +1,5 @@ +import { show, hide } from '@utils'; + +export function showFusion() { + show(document.querySelector('.fusion')); +} diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 472d6ebc9..7a529b41e 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -41,6 +41,9 @@ import { acceptEmailSuggestion, discardEmailSuggestionBox } from '../new_design/user-sign_up'; +import { + showFusion +} from '../new_design/fc-fusion'; // This is the global application namespace where we expose helpers used from rails views const DS = { @@ -49,6 +52,7 @@ const DS = { showMotivation, motivationCancel, showImportJustificatif, + showFusion, replaceSemicolonByComma, acceptEmailSuggestion, discardEmailSuggestionBox diff --git a/app/models/france_connect_information.rb b/app/models/france_connect_information.rb index 49f6fd7d9..06180a43a 100644 --- a/app/models/france_connect_information.rb +++ b/app/models/france_connect_information.rb @@ -52,4 +52,8 @@ class FranceConnectInformation < ApplicationRecord def valid_for_merge? (MERGE_VALIDITY.ago < merge_token_created_at) && user_id.nil? end + + def delete_merge_token! + update(merge_token: nil, merge_token_created_at: nil) + end end diff --git a/app/views/france_connect/particulier/merge.html.haml b/app/views/france_connect/particulier/merge.html.haml index 6ec910cdb..1ee48aa00 100644 --- a/app/views/france_connect/particulier/merge.html.haml +++ b/app/views/france_connect/particulier/merge.html.haml @@ -10,3 +10,24 @@ Votre compte FranceConnect utilise #{@fci.email_france_connect} comme email de contact. %br Or il existe un compte sur #{APPLICATION_NAME} avec cet email. + + .form.mt-2 + %label Ce compte #{@fci.email_france_connect} vous appartient-il ? + %fieldset.radios + %label{ onclick: "DS.showFusion(event);" } + = radio_button_tag :value, true, false, autocomplete: "off" + Oui + + %label + = radio_button_tag :value, false, false, autocomplete: "off" + Non + + .fusion.hidden + %p Pour les fusionner, entrez votre mot de passe + + = form_tag france_connect_particulier_merge_with_existing_account_path, remote: true, class: 'mt-2 form' do + = hidden_field_tag :merge_token, @fci.merge_token + = hidden_field_tag :email, @fci.email_france_connect + = label_tag :password, 'Mot de passe (8 caractères minimum)' + = password_field_tag :password, nil, autocomplete: 'current-password' + = submit_tag 'Fusionner les comptes', class: 'button primary' diff --git a/config/routes.rb b/config/routes.rb index 3560cc97a..8d27d54db 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -125,6 +125,7 @@ Rails.application.routes.draw do get 'particulier' => 'particulier#login' get 'particulier/callback' => 'particulier#callback' get 'particulier/merge/:merge_token' => 'particulier#merge', as: :particulier_merge + post 'particulier/merge_with_existing_account' => 'particulier#merge_with_existing_account' end namespace :champs do diff --git a/spec/controllers/france_connect/particulier_controller_spec.rb b/spec/controllers/france_connect/particulier_controller_spec.rb index a67eadbac..172bb9f4e 100644 --- a/spec/controllers/france_connect/particulier_controller_spec.rb +++ b/spec/controllers/france_connect/particulier_controller_spec.rb @@ -136,16 +136,7 @@ describe FranceConnect::ParticulierController, type: :controller do end end - describe '#merge' do - let(:fci) { FranceConnectInformation.create!(user_info) } - let(:merge_token) { fci.create_merge_token! } - - subject { get :merge, params: { merge_token: merge_token } } - - context 'when the merge token is valid' do - it { expect(subject).to have_http_status(:ok) } - end - + RSpec.shared_examples "a method that needs a valid merge token" do context 'when the merge token is invalid' do before do merge_token @@ -153,10 +144,29 @@ describe FranceConnect::ParticulierController, type: :controller do end it do - expect(subject).to redirect_to root_path + if format == :js + subject + expect(response.body).to eq("window.location.href='/'") + else + expect(subject).to redirect_to root_path + end expect(flash.alert).to eq('Votre compte FranceConnect a expiré, veuillez recommencer.') end end + end + + describe '#merge' do + let(:fci) { FranceConnectInformation.create!(user_info) } + let(:merge_token) { fci.create_merge_token! } + let(:format) { :html } + + subject { get :merge, params: { merge_token: merge_token } } + + context 'when the merge token is valid' do + it { expect(subject).to have_http_status(:ok) } + end + + it_behaves_like "a method that needs a valid merge token" context 'when the merge token does not exist' do let(:merge_token) { 'i do not exist' } @@ -167,4 +177,56 @@ describe FranceConnect::ParticulierController, type: :controller do end end end + + describe '#merge_with_existing_account' do + let(:fci) { FranceConnectInformation.create!(user_info) } + let(:merge_token) { fci.create_merge_token! } + let(:email) { 'EXISTING_account@a.com ' } + let(:password) { 'my-s3cure-p4ssword' } + let(:format) { :js } + + subject { post :merge_with_existing_account, params: { merge_token: merge_token, email: email, password: password }, format: format } + + it_behaves_like "a method that needs a valid merge token" + + context 'when the credentials are ok' do + let!(:user) { create(:user, email: email, password: password) } + + it 'merges the account, signs in, and delete the merge token' do + subject + fci.reload + + expect(fci.user).to eq(user) + expect(fci.merge_token).to be_nil + expect(controller.current_user).to eq(user) + end + + context 'but the targeted user is an instructeur' do + let!(:user) { create(:instructeur, email: email, password: password).user } + + it 'redirects to the root page' do + subject + fci.reload + + expect(fci.user).to be_nil + expect(fci.merge_token).not_to be_nil + expect(controller.current_user).to be_nil + end + end + end + + context 'when the credentials are not ok' do + let!(:user) { create(:user, email: email, password: 'another password #$21$%%') } + + it 'increases the failed attempts counter' do + subject + fci.reload + + expect(fci.user).to be_nil + expect(fci.merge_token).not_to be_nil + expect(controller.current_user).to be_nil + expect(user.reload.failed_attempts).to eq(1) + end + end + end end From ce40e1127d1461d899e4efc24a9a342eb4f0477d Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 13 Oct 2021 09:26:54 +0200 Subject: [PATCH 38/41] merge with another new account --- .../france_connect/particulier_controller.rb | 16 ++++++++++++- app/javascript/new_design/fc-fusion.js | 6 +++++ app/javascript/packs/application.js | 4 +++- .../particulier/merge.html.haml | 12 +++++++++- config/routes.rb | 1 + .../particulier_controller_spec.rb | 23 +++++++++++++++++++ 6 files changed, 59 insertions(+), 3 deletions(-) diff --git a/app/controllers/france_connect/particulier_controller.rb b/app/controllers/france_connect/particulier_controller.rb index e856a84fa..b025afd60 100644 --- a/app/controllers/france_connect/particulier_controller.rb +++ b/app/controllers/france_connect/particulier_controller.rb @@ -1,6 +1,6 @@ class FranceConnect::ParticulierController < ApplicationController before_action :redirect_to_login_if_fc_aborted, only: [:callback] - before_action :securely_retrieve_fci, only: [:merge, :merge_with_existing_account] + before_action :securely_retrieve_fci, only: [:merge, :merge_with_existing_account, :merge_with_new_account] def login if FranceConnectService.enabled? @@ -63,6 +63,20 @@ class FranceConnect::ParticulierController < ApplicationController end end + def merge_with_new_account + user = User.find_by(email: sanitized_email_params) + + if user.nil? + @fci.associate_user!(sanitized_email_params) + @fci.delete_merge_token! + + flash.notice = "Les comptes FranceConnect et #{APPLICATION_NAME} sont à présent fusionnés" + connect_france_connect_particulier(@fci.user) + else + # TODO + end + end + private def securely_retrieve_fci diff --git a/app/javascript/new_design/fc-fusion.js b/app/javascript/new_design/fc-fusion.js index bb1c904c3..8b23b0263 100644 --- a/app/javascript/new_design/fc-fusion.js +++ b/app/javascript/new_design/fc-fusion.js @@ -2,4 +2,10 @@ import { show, hide } from '@utils'; export function showFusion() { show(document.querySelector('.fusion')); + hide(document.querySelector('.new-account')); +} + +export function showNewAccount() { + hide(document.querySelector('.fusion')); + show(document.querySelector('.new-account')); } diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 7a529b41e..50a2b3a5b 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -42,7 +42,8 @@ import { discardEmailSuggestionBox } from '../new_design/user-sign_up'; import { - showFusion + showFusion, + showNewAccount } from '../new_design/fc-fusion'; // This is the global application namespace where we expose helpers used from rails views @@ -53,6 +54,7 @@ const DS = { motivationCancel, showImportJustificatif, showFusion, + showNewAccount, replaceSemicolonByComma, acceptEmailSuggestion, discardEmailSuggestionBox diff --git a/app/views/france_connect/particulier/merge.html.haml b/app/views/france_connect/particulier/merge.html.haml index 1ee48aa00..25f0ff021 100644 --- a/app/views/france_connect/particulier/merge.html.haml +++ b/app/views/france_connect/particulier/merge.html.haml @@ -18,7 +18,7 @@ = radio_button_tag :value, true, false, autocomplete: "off" Oui - %label + %label{ onclick: "DS.showNewAccount(event);" } = radio_button_tag :value, false, false, autocomplete: "off" Non @@ -31,3 +31,13 @@ = label_tag :password, 'Mot de passe (8 caractères minimum)' = password_field_tag :password, nil, autocomplete: 'current-password' = submit_tag 'Fusionner les comptes', class: 'button primary' + + .new-account.hidden + %p Donnez-nous alors le mail que #{APPLICATION_NAME} utilisera pour vous contacter + + = form_tag france_connect_particulier_merge_with_new_account_path, remote: true, class: 'mt-2 form' do + = hidden_field_tag :merge_token, @fci.merge_token + = label_tag :email, 'Email (nom@site.com)' + = email_field_tag :email + = submit_tag 'Utiliser ce mail', class: 'button primary' + diff --git a/config/routes.rb b/config/routes.rb index 8d27d54db..38bb0241d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -126,6 +126,7 @@ Rails.application.routes.draw do get 'particulier/callback' => 'particulier#callback' get 'particulier/merge/:merge_token' => 'particulier#merge', as: :particulier_merge post 'particulier/merge_with_existing_account' => 'particulier#merge_with_existing_account' + post 'particulier/merge_with_new_account' => 'particulier#merge_with_new_account' end namespace :champs do diff --git a/spec/controllers/france_connect/particulier_controller_spec.rb b/spec/controllers/france_connect/particulier_controller_spec.rb index 172bb9f4e..45a4d7635 100644 --- a/spec/controllers/france_connect/particulier_controller_spec.rb +++ b/spec/controllers/france_connect/particulier_controller_spec.rb @@ -229,4 +229,27 @@ describe FranceConnect::ParticulierController, type: :controller do end end end + + describe '#merge_with_new_account' do + let(:fci) { FranceConnectInformation.create!(user_info) } + let(:merge_token) { fci.create_merge_token! } + let(:email) { ' Account@a.com ' } + let(:format) { :js } + + subject { post :merge_with_new_account, params: { merge_token: merge_token, email: email }, format: format } + + it_behaves_like "a method that needs a valid merge token" + + context 'when the email does not belong to any user' do + it 'creates the account, signs in, and delete the merge token' do + subject + fci.reload + + expect(fci.user.email).to eq(email.downcase.strip) + expect(fci.merge_token).to be_nil + expect(controller.current_user).to eq(fci.user) + expect(response.body).to include("window.location.href='/'") + end + end + end end From 933d7b8c8da7c93219483221cdad7fa78d39bf74 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 13 Oct 2021 01:08:57 +0200 Subject: [PATCH 39/41] merge with another preexisting account --- .../france_connect/particulier_controller.rb | 3 ++- app/javascript/new_design/fc-fusion.js | 8 ++++++++ app/javascript/packs/application.js | 4 +++- .../_password_confirmation.html.haml | 12 ++++++++++++ .../france_connect/particulier/merge.html.haml | 1 + .../particulier/merge_with_new_account.js.erb | 3 +++ .../particulier_controller_spec.rb | 17 +++++++++++++++++ 7 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 app/views/france_connect/particulier/_password_confirmation.html.haml create mode 100644 app/views/france_connect/particulier/merge_with_new_account.js.erb diff --git a/app/controllers/france_connect/particulier_controller.rb b/app/controllers/france_connect/particulier_controller.rb index b025afd60..9d68e60ac 100644 --- a/app/controllers/france_connect/particulier_controller.rb +++ b/app/controllers/france_connect/particulier_controller.rb @@ -73,7 +73,8 @@ class FranceConnect::ParticulierController < ApplicationController flash.notice = "Les comptes FranceConnect et #{APPLICATION_NAME} sont à présent fusionnés" connect_france_connect_particulier(@fci.user) else - # TODO + @email = sanitized_email_params + @merge_token = merge_token_params end end diff --git a/app/javascript/new_design/fc-fusion.js b/app/javascript/new_design/fc-fusion.js index 8b23b0263..e9d4b7628 100644 --- a/app/javascript/new_design/fc-fusion.js +++ b/app/javascript/new_design/fc-fusion.js @@ -3,9 +3,17 @@ import { show, hide } from '@utils'; export function showFusion() { show(document.querySelector('.fusion')); hide(document.querySelector('.new-account')); + hide(document.querySelector('.new-account-password-confirmation')); } export function showNewAccount() { hide(document.querySelector('.fusion')); show(document.querySelector('.new-account')); + hide(document.querySelector('.new-account-password-confirmation')); +} + +export function showNewAccountPasswordConfirmation() { + hide(document.querySelector('.fusion')); + hide(document.querySelector('.new-account')); + show(document.querySelector('.new-account-password-confirmation')); } diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 50a2b3a5b..bfaa7cda4 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -43,7 +43,8 @@ import { } from '../new_design/user-sign_up'; import { showFusion, - showNewAccount + showNewAccount, + showNewAccountPasswordConfirmation } from '../new_design/fc-fusion'; // This is the global application namespace where we expose helpers used from rails views @@ -55,6 +56,7 @@ const DS = { showImportJustificatif, showFusion, showNewAccount, + showNewAccountPasswordConfirmation, replaceSemicolonByComma, acceptEmailSuggestion, discardEmailSuggestionBox diff --git a/app/views/france_connect/particulier/_password_confirmation.html.haml b/app/views/france_connect/particulier/_password_confirmation.html.haml new file mode 100644 index 000000000..c83addb8c --- /dev/null +++ b/app/views/france_connect/particulier/_password_confirmation.html.haml @@ -0,0 +1,12 @@ +%p + Le compte #{email} existe déjà sur #{APPLICATION_NAME} + %br + entrez votre mot de passe pour fusionner les comptes + += form_tag france_connect_particulier_merge_with_existing_account_path, remote: true, class: 'mt-2 form' do + = hidden_field_tag :merge_token, merge_token + = hidden_field_tag :email, email + = label_tag :password, 'Mot de passe (8 caractères minimum)' + = password_field_tag :password, nil, autocomplete: 'current-password' + = button_tag 'revenir en arrière', type: 'button', class: 'button secondary', onclick: 'DS.showNewAccount(event);' + = submit_tag 'Fusionner les comptes', class: 'button primary' diff --git a/app/views/france_connect/particulier/merge.html.haml b/app/views/france_connect/particulier/merge.html.haml index 25f0ff021..f2acdb168 100644 --- a/app/views/france_connect/particulier/merge.html.haml +++ b/app/views/france_connect/particulier/merge.html.haml @@ -41,3 +41,4 @@ = email_field_tag :email = submit_tag 'Utiliser ce mail', class: 'button primary' + .new-account-password-confirmation.hidden diff --git a/app/views/france_connect/particulier/merge_with_new_account.js.erb b/app/views/france_connect/particulier/merge_with_new_account.js.erb new file mode 100644 index 000000000..cea1ca67a --- /dev/null +++ b/app/views/france_connect/particulier/merge_with_new_account.js.erb @@ -0,0 +1,3 @@ +<%= render_to_element('.new-account-password-confirmation', partial: 'password_confirmation', locals: { email: @email, merge_token: @merge_token }) %> + +DS.showNewAccountPasswordConfirmation(); diff --git a/spec/controllers/france_connect/particulier_controller_spec.rb b/spec/controllers/france_connect/particulier_controller_spec.rb index 45a4d7635..3ff97c96b 100644 --- a/spec/controllers/france_connect/particulier_controller_spec.rb +++ b/spec/controllers/france_connect/particulier_controller_spec.rb @@ -251,5 +251,22 @@ describe FranceConnect::ParticulierController, type: :controller do expect(response.body).to include("window.location.href='/'") end end + + context 'when an account with the same email exists' do + let!(:user) { create(:user, email: email) } + + render_views + + it 'asks for the corresponding password' do + subject + fci.reload + + expect(fci.user).to be_nil + expect(fci.merge_token).not_to be_nil + expect(controller.current_user).to be_nil + + expect(response.body).to include('entrez votre mot de passe') + end + end end end From bb83fd2f189e056b701746fadf57ee6de95d9cfe Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 13 Oct 2021 15:45:57 +0200 Subject: [PATCH 40/41] To make an old test work, no idea --- app/controllers/france_connect/particulier_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/france_connect/particulier_controller.rb b/app/controllers/france_connect/particulier_controller.rb index 9d68e60ac..40625c23f 100644 --- a/app/controllers/france_connect/particulier_controller.rb +++ b/app/controllers/france_connect/particulier_controller.rb @@ -26,6 +26,7 @@ class FranceConnect::ParticulierController < ApplicationController user = fci.user if user.can_france_connect? + fci.update(updated_at: Time.zone.now) connect_france_connect_particulier(user) else fci.destroy From 46fd15416b7b9c1299d37bd35124b142c36bd176 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 13 Oct 2021 16:51:00 +0200 Subject: [PATCH 41/41] add end to end test --- .../_password_confirmation.html.haml | 2 +- .../particulier/merge.html.haml | 4 +- .../france_connect_particulier_spec.rb | 61 +++++++++++++++++-- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/app/views/france_connect/particulier/_password_confirmation.html.haml b/app/views/france_connect/particulier/_password_confirmation.html.haml index c83addb8c..0bc209dff 100644 --- a/app/views/france_connect/particulier/_password_confirmation.html.haml +++ b/app/views/france_connect/particulier/_password_confirmation.html.haml @@ -7,6 +7,6 @@ = hidden_field_tag :merge_token, merge_token = hidden_field_tag :email, email = label_tag :password, 'Mot de passe (8 caractères minimum)' - = password_field_tag :password, nil, autocomplete: 'current-password' + = password_field_tag :password, nil, autocomplete: 'current-password', id: 'password-for-another-account' = button_tag 'revenir en arrière', type: 'button', class: 'button secondary', onclick: 'DS.showNewAccount(event);' = submit_tag 'Fusionner les comptes', class: 'button primary' diff --git a/app/views/france_connect/particulier/merge.html.haml b/app/views/france_connect/particulier/merge.html.haml index f2acdb168..c8f4ae656 100644 --- a/app/views/france_connect/particulier/merge.html.haml +++ b/app/views/france_connect/particulier/merge.html.haml @@ -15,11 +15,11 @@ %label Ce compte #{@fci.email_france_connect} vous appartient-il ? %fieldset.radios %label{ onclick: "DS.showFusion(event);" } - = radio_button_tag :value, true, false, autocomplete: "off" + = radio_button_tag :value, true, false, autocomplete: "off", id: 'it-is-mine' Oui %label{ onclick: "DS.showNewAccount(event);" } - = radio_button_tag :value, false, false, autocomplete: "off" + = radio_button_tag :value, false, false, autocomplete: "off", id: 'it-is-not-mine' Non .fusion.hidden diff --git a/spec/features/france_connect/france_connect_particulier_spec.rb b/spec/features/france_connect/france_connect_particulier_spec.rb index 6c5b98520..4498f8031 100644 --- a/spec/features/france_connect/france_connect_particulier_spec.rb +++ b/spec/features/france_connect/france_connect_particulier_spec.rb @@ -34,18 +34,67 @@ feature 'France Connect Particulier Connexion' do allow(FranceConnectService).to receive(:retrieve_user_informations_particulier).and_return(france_connect_information) end - context 'when is the first connexion' do + context 'when no user is linked' do let(:france_connect_information) { build(:france_connect_information, user_info) } - before { page.find('.france-connect-login-button').click } + context 'and no user has the same email' do + before { page.find('.france-connect-login-button').click } - scenario 'he is redirected to user dossiers page' do - expect(page).to have_content('Dossiers') - expect(User.find_by(email: email)).not_to be nil + scenario 'he is redirected to user dossiers page' do + expect(page).to have_content('Dossiers') + expect(User.find_by(email: email)).not_to be nil + end + end + + context 'and an user exists with the same email' do + let!(:user) { create(:user, email: email, password: 'my-s3cure-p4ssword') } + + before do + page.find('.france-connect-login-button').click + end + + scenario 'he is redirected to the merge page' do + expect(page).to have_content('Fusion des comptes') + end + + scenario 'it merges its account' do + page.find('#it-is-mine').click + fill_in 'password', with: 'my-s3cure-p4ssword' + click_on 'Fusionner les comptes' + + expect(page).to have_content('Dossiers') + end + + scenario 'it uses another email that belongs to nobody' do + page.find('#it-is-not-mine').click + fill_in 'email', with: 'new_email@a.com' + click_on 'Utiliser ce mail' + + expect(page).to have_content('Dossiers') + end + + context 'and the user wants an email that belongs to another account', js: true do + let!(:another_user) { create(:user, email: 'an_existing_email@a.com', password: 'my-s3cure-p4ssword') } + + scenario 'it uses another email that belongs to another account' do + page.find('#it-is-not-mine').click + fill_in 'email', with: 'an_existing_email@a.com' + click_on 'Utiliser ce mail' + + expect(page).to have_css('#password-for-another-account', visible: true) + + within '.new-account-password-confirmation' do + fill_in 'password', with: 'my-s3cure-p4ssword' + click_on 'Fusionner les comptes' + end + + expect(page).to have_content('Dossiers') + end + end end end - context 'when is not the first connexion' do + context 'when a user is linked' do let!(:france_connect_information) do create(:france_connect_information, :with_user, user_info.merge(created_at: Time.zone.parse('12/12/2012'), updated_at: Time.zone.parse('12/12/2012'))) end