Merge pull request #6549 from betagouv/main

2021-10-14-01
This commit is contained in:
LeSim 2021-10-14 17:46:15 +02:00 committed by GitHub
commit a14ea5f0f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
103 changed files with 1208 additions and 153 deletions

View file

@ -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)

View file

@ -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;
}
}
}

View file

@ -241,6 +241,7 @@
}
input[type=email],
input[type=password],
input[type=number],
input[type=tel], {
max-width: 500px;
@ -356,6 +357,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;

View file

@ -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;
}

View file

@ -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)

View file

@ -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, :merge_with_existing_account, :merge_with_new_account]
def login
if FranceConnectService.enabled?
@ -11,23 +12,88 @@ class FranceConnect::ParticulierController < ApplicationController
def callback
fci = FranceConnectService.find_or_retrieve_france_connect_information(params[:code])
fci.associate_user!
if fci.user && !fci.user.can_france_connect?
fci.destroy
redirect_to new_user_session_path, alert: t('errors.messages.france_connect.forbidden_html', reset_link: new_user_password_path)
return
if fci.user.nil?
preexisting_unlinked_user = User.find_by(email: fci.email_france_connect.downcase)
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
user = fci.user
if user.can_france_connect?
fci.update(updated_at: Time.zone.now)
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
connect_france_connect_particulier(fci.user)
rescue Rack::OAuth2::Client::Error => e
Rails.logger.error e.message
redirect_france_connect_error_connection
end
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
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
@email = sanitized_email_params
@merge_token = merge_token_params
end
end
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.'
respond_to do |format|
format.html { redirect_to root_path }
format.js { render js: ajax_redirect(root_path) }
end
end
end
def redirect_to_login_if_fc_aborted
if params[:code].blank?
redirect_to new_user_session_path
@ -39,23 +105,32 @@ 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))
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
flash.alert = t('errors.messages.france_connect.connexion')
redirect_to(new_user_session_path)
end
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

View file

@ -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
@ -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

View file

@ -1869,6 +1869,11 @@ enum TypeDeChamp {
"""
civilite
"""
Données de la Caisse nationale des allocations familiales
"""
cnaf
"""
Communes
"""

View file

@ -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')

View file

@ -0,0 +1,19 @@
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'));
}

View file

@ -41,6 +41,11 @@ import {
acceptEmailSuggestion,
discardEmailSuggestionBox
} from '../new_design/user-sign_up';
import {
showFusion,
showNewAccount,
showNewAccountPasswordConfirmation
} from '../new_design/fc-fusion';
// This is the global application namespace where we expose helpers used from rails views
const DS = {
@ -49,6 +54,9 @@ const DS = {
showMotivation,
motivationCancel,
showImportJustificatif,
showFusion,
showNewAccount,
showNewAccountPasswordConfirmation,
replaceSemicolonByComma,
acceptEmailSuggestion,
discardEmailSuggestionBox

View file

@ -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

View file

@ -1,4 +1,4 @@
class APIParticulier::CNAFAdapter
class APIParticulier::CnafAdapter
class InvalidSchemaError < ::StandardError
def initialize(errors)
super(errors.map(&:to_json).join("\n"))

View file

@ -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}

View file

@ -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

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -0,0 +1,52 @@
# == 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
# 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? && 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
def blank?
external_id.nil?
end
def fetch_external_data?
true
end
def fetch_external_data
if valid?
APIParticulier::CnafAdapter.new(
procedure.api_particulier_token,
numero_allocataire,
code_postal,
procedure.api_particulier_sources
).to_params
end
end
def external_id
if numero_allocataire.present? && code_postal.present?
{ code_postal: code_postal, numero_allocataire: numero_allocataire }.to_json
end
end
end

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -9,6 +9,7 @@
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer

View file

@ -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) }

View file

@ -10,35 +10,50 @@
# 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
# user_id :integer not null
# 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
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
def associate_user!(email)
begin
user = User.create!(
email: email.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)
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
def delete_merge_token!
update(merge_token: nil, merge_token_created_at: nil)
end
end

View file

@ -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))

View file

@ -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

View file

@ -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
@ -291,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

View file

@ -0,0 +1,2 @@
class TypesDeChamp::CnafTypeDeChamp < TypesDeChamp::TextTypeDeChamp
end

View file

@ -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', 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'

View file

@ -0,0 +1,44 @@
= content_for :title, "Fusion des comptes FC et #{APPLICATION_NAME}"
.container
%h1.page-title Fusion des comptes FranceConnect et #{APPLICATION_NAME}
%p
Bonjour,
%br
%br
Votre compte FranceConnect utilise <b class='bold'>#{@fci.email_france_connect}</b> 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", id: 'it-is-mine'
Oui
%label{ onclick: "DS.showNewAccount(event);" }
= radio_button_tag :value, false, false, autocomplete: "off", id: 'it-is-not-mine'
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'
.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'
.new-account-password-confirmation.hidden

View file

@ -0,0 +1,3 @@
<%= render_to_element('.new-account-password-confirmation', partial: 'password_confirmation', locals: { email: @email, merge_token: @merge_token }) %>
DS.showNewAccountPasswordConfirmation();

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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]

View file

@ -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

View file

@ -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] }

View file

@ -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)

View file

@ -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

View file

@ -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) }

View file

@ -2,5 +2,3 @@
{ required: champ.mandatory? },
'on',
'off'
%br

View file

@ -1,6 +1,4 @@
%fieldset.radios
%legend.mandatory-explanation
Sélectionnez une des deux valeurs
%label
= form.radio_button :value, true
Oui

View file

@ -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'

View file

@ -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

View file

@ -0,0 +1,32 @@
en:
api_particulier:
providers:
cnaf:
libelle: Caisse nationale dallocations 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 didentité
complementIdentiteGeo: complément didentité 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

View file

@ -2,12 +2,14 @@ fr:
api_particulier:
providers:
cnaf:
libelle: Caisse dallocations familiales (CAF)
libelle: Caisse nationale dallocations 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

View file

@ -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 dune autre de vos procedures publiées. Si vous publiez cette procedure, lancienne 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."

View file

@ -280,6 +280,12 @@ fr:
taken: est déjà utilisé par une démarche. Vous ne pouvez pas lutiliser car il appartient à un autre administrateur.
taken_can_be_claimed: est identique à celui dune autre de vos démarches publiées. Si vous publiez cette démarche, lancienne sera dépubliée et ne sera plus accessible au public. Les utilisateurs qui ont commencé un brouillon vont pouvoir le déposer.
invalid: nest 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."

View file

@ -1,6 +0,0 @@
fr:
activerecord:
attributes:
champ:
value: La valeur du champ
piece_justificative_file: La pièce justificative

View file

@ -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'

View file

@ -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 dallocations familiales"

View file

@ -0,0 +1,16 @@
fr:
shared:
dossiers:
editable_champs:
cnaf:
numero_allocataire_label: Le numéro dallocataire 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 lallocataire 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 dallocations familiales"

View file

@ -124,6 +124,9 @@ 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
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

View file

@ -0,0 +1,5 @@
class AddValueJSONColumnToChamp < ActiveRecord::Migration[6.1]
def change
add_column :champs, :value_json, :jsonb
end
end

View file

@ -0,0 +1,5 @@
class RemoveUserIdNotNullConstraintToFranceConnectInformation < ActiveRecord::Migration[6.1]
def change
change_column_null(:france_connect_informations, :user_id, true)
end
end

View file

@ -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

View file

@ -0,0 +1,5 @@
class AddForeignKeyToAvisDossierId < ActiveRecord::Migration[6.1]
def change
add_foreign_key :avis, :dossiers
end
end

View file

@ -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"
@ -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"
@ -472,11 +473,14 @@ ActiveRecord::Schema.define(version: 2021_10_06_164955) 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
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
@ -813,6 +817,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"

View file

@ -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,37 +49,68 @@ 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 do
expect(response).to redirect_to(new_user_session_path)
expect(flash[:alert]).to be_present
end
end
it { expect(flash[:alert]).to be_present }
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
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 }
@ -104,4 +135,138 @@ describe FranceConnect::ParticulierController, type: :controller do
it { expect(flash[:alert]).to be_present }
end
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
fci.update(merge_token_created_at: 2.years.ago)
end
it do
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' }
it do
expect(subject).to redirect_to root_path
expect(flash.alert).to eq('Votre compte FranceConnect a expiré, veuillez recommencer.')
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
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
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

View file

@ -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]

View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,167 @@
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 didentité')
check('complément didentité 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 dallocataire 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
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 didentité 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 didentité 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

View file

@ -21,62 +21,85 @@ 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))
allow(FranceConnectService).to receive(:retrieve_user_informations_particulier).and_return(france_connect_information)
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)
context 'when no user is linked' do
let(:france_connect_information) { build(:france_connect_information, user_info) }
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
end
end
before do
page.find('.france-connect-login-button').click
end
context 'and an user exists with the same email' do
let!(:user) { create(:user, email: email, password: 'my-s3cure-p4ssword') }
scenario 'he is redirected to user dossiers page' do
expect(page).to have_content('Dossiers')
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,
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')

View file

@ -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')
@ -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

View file

@ -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: ''

View file

@ -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: ''

View file

@ -1,7 +1,7 @@
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") }
before { stub_const("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api") }
describe '#to_params' do
let(:api_particulier_token) { '29eb50b65f64e8e00c0847a8bbcbd150e1f847' }
@ -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

View file

@ -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

View file

@ -0,0 +1,103 @@
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
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)) }
let(:validation_context) { :create }
subject { champ.valid?(validation_context) }
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
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
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

View file

@ -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) }
@ -28,7 +34,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

View file

@ -9,24 +9,52 @@ 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
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 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! }
context 'when the merge token is too old' do
before { fci.merge_token_created_at = (FranceConnectInformation::MERGE_VALIDITY + 1.minute).ago }
before { subject }
it { is_expected.to be(false) }
end
end
it { expect(fci.user).to eq(user) }
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

Some files were not shown because too many files have changed in this diff Show more