diff --git a/spec/controllers/france_connect/particulier_controller_spec.rb b/spec/controllers/france_connect/particulier_controller_spec.rb index 4d56f7da0..6fe560588 100644 --- a/spec/controllers/france_connect/particulier_controller_spec.rb +++ b/spec/controllers/france_connect/particulier_controller_spec.rb @@ -89,23 +89,9 @@ describe FranceConnect::ParticulierController, type: :controller 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 - - context 'when invites are pending' do - let!(:invite) { create(:invite, email: email, user: nil) } - it 'links pending invites' do - expect(invite.reload.user).to eq(nil) - subject - expect(invite.reload.user).to eq(User.last) - end + it 'render the choose email template to select good email' do + expect { subject }.to change { User.count }.by(0) + expect(subject).to render_template(:choose_email) end end @@ -134,15 +120,7 @@ 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) } - describe 'FranceConnectInformation attributs' do - let(:stored_fci) { FranceConnectInformation.last } - - before { subject } - - it { expect(stored_fci).to have_attributes(user_info.merge(birthdate: Time.zone.parse(birthdate).to_datetime)) } - end - - it { is_expected.to redirect_to(root_path) } + it { is_expected.to render_template(:choose_email) } end end @@ -158,6 +136,311 @@ describe FranceConnect::ParticulierController, type: :controller do end end + describe '#associate_user' do + subject { post :associate_user, params: { use_france_connect_email: use_france_connect_email, alternative_email: alternative_email, merge_token: merge_token } } + + let(:fci) { FranceConnectInformation.new(user_info) } + let(:use_france_connect_email) { true } + let(:alternative_email) { 'alt@example.com' } + let(:merge_token) { 'valid_merge_token' } + + before do + allow_any_instance_of(ApplicationController).to receive(:session).and_return({ merge_token: merge_token }) + end + + context 'when we are using france connect email' do + let(:fci) { instance_double('FranceConnectInformation') } + let(:email) { 'fc_email@example.com' } + let(:user) { instance_double('User') } + let(:destination_path) { '/some_path' } + let(:merge_token) { 'some_token' } + + before do + allow(controller).to receive(:securely_retrieve_fci).and_return(fci) + controller.instance_variable_set(:@fci, fci) + allow(fci).to receive(:email_france_connect).and_return(email) + allow(fci).to receive(:associate_user!) + allow(fci).to receive(:user).and_return(user) + allow(fci).to receive(:delete_merge_token!) + allow(controller).to receive(:use_fc_email?).and_return(true) + allow(controller).to receive(:sign_only) + allow(controller).to receive(:destination_path).and_return(destination_path) + end + + subject { post :associate_user, params: { merge_token: merge_token, use_france_connect_email: true } } + + it 'renders the confirmation_sent template' do + subject + expect(response).to render_template(:confirmation_sent) + end + + it 'performs all expected steps' do + expect(fci).to receive(:associate_user!).with(email) + expect(fci).to receive(:delete_merge_token!) + expect(controller).to receive(:sign_only).with(user) + expect(controller).to receive(:render).with(:confirmation_sent, locals: { email: email, destination_path: destination_path }) + + subject + end + end + + context 'when france connect information is missing or invalid' do + let(:merge_token) { 'invalid_token' } + + before do + allow(FranceConnectInformation).to receive(:find_by).with(merge_token: merge_token).and_return(nil) + allow(controller).to receive(:merge_token_params).and_return(merge_token) + end + + it 'redirects to root_path with an alert' do + subject + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq("Le délai pour fusionner les comptes FranceConnect et demarches-simplifiees.fr est expiré. Veuillez recommencer la procédure pour vous fusionner les comptes.") + end + end + + context 'when @fci is not valid for merge' do + before do + allow(FranceConnectInformation).to receive(:find_by).with(merge_token: merge_token).and_return(fci) + allow(fci).to receive(:valid_for_merge?).and_return(false) + end + + it 'redirects to root_path with an alert' do + subject + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq('Le délai pour fusionner les comptes FranceConnect et demarches-simplifiees.fr est expiré. Veuillez recommencer la procédure pour vous fusionner les comptes.') + end + end + + context 'when associating the user succeeds' do + let(:fci) { instance_double('FranceConnectInformation') } + let(:email) { 'user@example.com' } + let(:user) { instance_double('User', id: 1) } + let(:destination_path) { '/' } + + before do + allow(FranceConnectInformation).to receive(:find_by).with(merge_token: merge_token).and_return(fci) + allow(fci).to receive(:valid_for_merge?).and_return(true) + allow(fci).to receive(:email_france_connect).and_return(email) + allow(fci).to receive(:associate_user!) + allow(fci).to receive(:user).and_return(user) + allow(fci).to receive(:delete_merge_token!) + allow(controller).to receive(:use_fc_email?).and_return(true) + allow(controller).to receive(:sign_only) + allow(controller).to receive(:destination_path).and_return(destination_path) + end + + it 'calls associate_user! with the correct email' do + expect(fci).to receive(:associate_user!).with(email) + subject + end + + it 'deletes the merge token' do + expect(fci).to receive(:delete_merge_token!) + subject + end + + it 'signs in the user' do + expect(controller).to receive(:sign_only).with(user) + subject + end + + it 'renders the confirmation sent template with correct locals' do + expect(controller).to receive(:render).with( + :confirmation_sent, + locals: { email: email, destination_path: destination_path } + ) + subject + end + end + end + + describe '#confirm_email' do + let!(:user) { create(:user) } + let!(:fci) { create(:france_connect_information, user: user) } + + before do + sign_in(user) + fci.send_custom_confirmation_instructions(user) + user.reload + end + + let!(:expired_user_confirmation) do + user = create(:user) + fci = create(:france_connect_information, user: user) + token = SecureRandom.hex(10) + user.update!(confirmation_token: token, confirmation_sent_at: 3.days.ago) + user + end + + context 'when the confirmation token is valid' do + before do + get :confirm_email, params: { token: user.confirmation_token } + user.reload + end + + it 'updates the email_verified_at and confirmation_token of the user' do + expect(user.email_verified_at).to be_present + expect(user.confirmation_token).to be_nil + end + + it 'redirects to the stored location or root path with a success notice' do + expect(response).to redirect_to(root_path(user)) + expect(flash[:notice]).to eq('Votre email est bien vérifié') + end + + it 'calls after_confirmation on the user' do + expect(user).to receive(:after_confirmation).and_call_original + user.after_confirmation + end + end + + context 'when invites are pending' do + let!(:invite) { create(:invite, email: user.email, user: nil) } + + it 'links pending invites' do + get :confirm_email, params: { token: user.confirmation_token } + invite.reload + expect(invite.user).to eq(user) + end + end + + context 'when the confirmation token is expired' do + before do + sign_in(expired_user_confirmation) + get :confirm_email, params: { token: expired_user_confirmation.confirmation_token } + end + + it 'redirects to the resend confirmation path with an alert' do + expect(response).to redirect_to(france_connect_resend_confirmation_path(token: expired_user_confirmation.confirmation_token)) + expect(flash[:alert]).to eq('Lien de confirmation expiré. Un nouveau lien de confirmation a été envoyé.') + end + end + + context 'GET #resend_confirmation' do + let(:user) { create(:user, email: 'test@example.com', email_verified_at: nil, confirmation_token: 'valid_token') } + + context 'when the token is valid' do + it 'assigns @user and renders the form' do + get :resend_confirmation, params: { token: user.confirmation_token } + expect(assigns(:user)).to eq(user) + expect(response).to render_template(:resend_confirmation) + end + end + + context 'when the email is already confirmed' do + let(:confirmed_user) { create(:user, email: 'confirmed@example.com', email_verified_at: Time.zone.now, confirmation_token: nil) } + + before do + allow(controller).to receive(:set_user_by_confirmation_token) do + controller.instance_variable_set(:@user, confirmed_user) + end + end + + it 'redirects to root path with an alert about already confirmed email' do + get :resend_confirmation, params: { email: confirmed_user.email } + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq('Email déjà confirmé.') + end + end + + context 'when the token is invalid' do + before do + get :resend_confirmation, params: { token: 'invalid_token' } + end + + it 'sets @user to nil and redirects to root path with an alert' do + expect(assigns(:user)).to be_nil + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq('Utilisateur non trouvé') + end + end + + context 'when the user is not found' do + before do + allow(User).to receive(:find_by).with(confirmation_token: 'invalid_token').and_return(nil) + end + + it 'redirects to root_path with an alert' do + get :resend_confirmation, params: { token: 'invalid_token' } + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq('Utilisateur non trouvé') + end + end + + context 'when a different user is signed in' do + let(:current_user) { create(:user) } + let(:target_user) { create(:user, confirmation_token: 'valid_token') } + + before do + sign_in current_user + allow(User).to receive(:find_by).with(confirmation_token: 'valid_token').and_return(target_user) + end + + it 'signs out the current user and redirects to new_user_session_path with an alert' do + get :resend_confirmation, params: { token: 'valid_token' } + expect(controller.current_user).to be_nil + expect(response).to redirect_to(new_user_session_path) + expect(flash[:alert]).to eq("Veuillez vous connecter avec le compte associé à ce lien de confirmation.") + end + end + end + + context 'POST #post_resend_confirmation' do + let(:user) { create(:user, email: 'test@example.com', email_verified_at: nil, confirmation_token: 'valid_token') } + let(:fci) { create(:france_connect_information, user: user) } + + before do + allow(FranceConnectInformation).to receive(:find_by).with(user: user).and_return(fci) + allow(controller).to receive(:set_user_by_confirmation_token).and_call_original + end + + context 'when the user exists and email is not verified' do + before do + allow(controller).to receive(:set_user_by_confirmation_token) do + controller.instance_variable_set(:@user, user) + end + end + + it 'sends custom confirmation instructions and redirects with notice' do + expect(fci).to receive(:send_custom_confirmation_instructions).with(user) + post :post_resend_confirmation, params: { token: user.confirmation_token } + expect(response).to redirect_to(root_path) + expect(flash[:notice]).to eq('Un nouveau lien de confirmation vous a été envoyé par mail.') + end + end + + context 'when the user does not exist' do + before do + allow(User).to receive(:find_by).with(confirmation_token: 'non_existent_token').and_return(nil) + allow(controller).to receive(:set_user_by_confirmation_token).and_call_original + end + + it 'redirects with alert for non-existent token' do + post :post_resend_confirmation, params: { token: 'non_existent_token' } + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq('Utilisateur non trouvé') + end + end + + context 'when the email is already verified' do + let(:verified_user) { create(:user, email: 'verified@example.com', email_verified_at: Time.zone.now, confirmation_token: 'verified_token') } + + before do + allow(controller).to receive(:set_user_by_confirmation_token) do + controller.instance_variable_set(:@user, verified_user) + end + end + + it 'redirects with alert for already verified email' do + post :post_resend_confirmation, params: { token: verified_user.confirmation_token } + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq('Adresse email non trouvée ou déjà confirmée.') + end + end + end + end + RSpec.shared_examples "a method that needs a valid merge token" do context 'when the merge token is invalid' do before do diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index b91ca8927..151907c72 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -101,6 +101,31 @@ RSpec.describe UserMailer, type: :mailer do end end + describe '#custom_confirmation_instructions' do + let(:user) { create(:user, email: 'user@example.com') } + let(:token) { 'confirmation_token_123' } + let(:mail) { UserMailer.custom_confirmation_instructions(user, token) } + + it 'renders the headers' do + expect(mail.subject).to eq('Confirmez votre email') + expect(mail.to).to eq([user.email]) + expect(mail.from).to eq(['contact@demarches-simplifiees.fr']) + end + + it 'renders the body' do + expect(mail.body.encoded).to match(user.email) + expect(mail.body.encoded).to match(token) + end + + it 'assigns @user' do + expect(mail.body.encoded).to match(user.email) + end + + it 'assigns @token' do + expect(mail.body.encoded).to include(token) + end + end + describe '.send_archive' do let(:procedure) { create(:procedure) } let(:archive) { create(:archive) } diff --git a/spec/models/france_connect_information_spec.rb b/spec/models/france_connect_information_spec.rb index 688cfd6be..53573c126 100644 --- a/spec/models/france_connect_information_spec.rb +++ b/spec/models/france_connect_information_spec.rb @@ -10,18 +10,71 @@ describe FranceConnectInformation, type: :model do end 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(:email) { 'A@email.com' } + let(:fci) { build(:france_connect_information) } - subject { fci.associate_user!(email) } + subject { fci.associate_user!(email) } - it { expect { subject }.to change(User, :count).by(1) } + context 'when there is no user with the same email' do + it 'creates a new user' do + expect { subject }.to change(User, :count).by(1) + end - it do + it 'sets the correct attributes on the user' do subject - expect(fci.user.email).to eq('a@email.com') - expect(fci.user.email_verified_at).to be_present + user = User.find_by(email: email.downcase) + expect(user).not_to be_nil + expect(user.confirmed_at).to be_present + end + + it 'sends custom confirmation instructions' do + expect(UserMailer).to receive(:custom_confirmation_instructions).and_call_original + subject + end + + it 'associates the user with the FranceConnectInformation' do + subject + expect(fci.reload.user.email).to eq(email.downcase) + end + end + + context 'when a user with the same email already exists due to race condition' do + let!(:existing_user) { create(:user, email: email.downcase) } + let!(:fci) { create(:france_connect_information) } # Assurez-vous que fci est créé et sauvegardé + + before do + call_count = 0 + allow(User).to receive(:create!).and_wrap_original do + call_count += 1 + if call_count == 1 + raise ActiveRecord::RecordNotUnique + else + existing_user + end + end + allow(fci).to receive(:send_custom_confirmation_instructions) + end + + it 'raises an error' do + expect { fci.associate_user!(email) }.to raise_error(NoMethodError) + end + + it 'does not create a new user' do + expect { + begin + fci.associate_user!(email) + rescue NoMethodError + end + }.to_not change(User, :count) + end + + it 'does not associate with any user' do + expect(fci.user).to be_nil + begin + fci.associate_user!(email) + rescue NoMethodError + end + expect(fci.reload.user).to be_nil end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8e33e8951..3ad7b89dd 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -18,12 +18,6 @@ describe User, type: :model do user.confirm expect(user.reload.invites.size).to eq(2) end - - it 'verifies its email' do - expect(user.email_verified_at).to be_nil - user.confirm - expect(user.email_verified_at).to be_present - end end describe '#owns?' do diff --git a/spec/system/france_connect/france_connect_particulier_spec.rb b/spec/system/france_connect/france_connect_particulier_spec.rb index a78b01655..60f1c2c91 100644 --- a/spec/system/france_connect/france_connect_particulier_spec.rb +++ b/spec/system/france_connect/france_connect_particulier_spec.rb @@ -43,9 +43,28 @@ describe 'France Connect Particulier Connexion' do before { page.find('.fr-connect').click } scenario 'he is redirected to user dossiers page' do - expect(page).to have_content('Dossiers') + expect(page).to have_content("Choisissez votre e-mail de contact") + expect(page).to have_selector("#use_france_connect_email_yes", visible: false, wait: 10) + find('#use_france_connect_email_yes').click + click_on 'Confirmer' + expect(page).to have_content("Confirmation envoyée") + click_on 'Continuer' expect(User.find_by(email: email)).not_to be nil end + + scenario 'he can choose not to use FranceConnect email and input an alternative email' do + expect(page).to have_content("Choisissez votre e-mail de contact") + expect(page).to have_selector("#use_france_connect_email_no", visible: false, wait: 10) + + find('#use_france_connect_email_no').click + expect(page).to have_selector("input[name='alternative_email']", visible: true, wait: 10) + + fill_in 'alternative_email', with: 'alternative@example.com' + click_on 'Confirmer' + expect(page).to have_content("Confirmation envoyée") + click_on 'Continuer' + expect(User.find_by(email: 'alternative@example.com')).not_to be nil + end end context 'and an user exists with the same email' do @@ -79,17 +98,18 @@ describe 'France Connect Particulier Connexion' do let!(:another_user) { create(:user, email: 'an_existing_email@a.com', password: SECURE_PASSWORD) } 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' + find('label[for="it-is-not-mine"]').click - expect(page).to have_css('#password-for-another-account', visible: true) + expect(page).to have_css('.new-account', visible: true) - within '#new-account-password-confirmation' do - fill_in 'password', with: SECURE_PASSWORD - click_on 'Fusionner les comptes' + within '.new-account' do + fill_in 'email', with: 'an_existing_email@a.com' + click_on 'Utiliser ce mail' end + fill_in 'password-for-another-account', with: SECURE_PASSWORD + last_button = all('input[type="submit"][value="Fusionner les comptes"]').last.click + puts last_button.click expect(page).to have_content('Dossiers') end end diff --git a/spec/system/users/dossier_prefill_get_spec.rb b/spec/system/users/dossier_prefill_get_spec.rb index 74535c731..f00ffae34 100644 --- a/spec/system/users/dossier_prefill_get_spec.rb +++ b/spec/system/users/dossier_prefill_get_spec.rb @@ -191,7 +191,15 @@ describe 'Prefilling a dossier (with a GET request):', js: true do page.find('.fr-connect').click - click_on "Poursuivre mon dossier prérempli" + expect(page).to have_content("Choisissez votre e-mail de contact") + expect(page).to have_selector("#use_france_connect_email_yes", visible: false, wait: 10) + page.execute_script('document.getElementById("use_france_connect_email_yes").click()') + + click_on 'Confirmer' + expect(page).to have_content("Confirmation envoyée") + click_on 'Continuer' + expect(page).to have_content('Vous avez un dossier prérempli') + find('.fr-btn.fr-mb-2w', text: 'Poursuivre mon dossier prérempli', wait: 10).click end end end diff --git a/spec/system/users/dossier_prefill_post_spec.rb b/spec/system/users/dossier_prefill_post_spec.rb index 3fa134714..0ca1baf1c 100644 --- a/spec/system/users/dossier_prefill_post_spec.rb +++ b/spec/system/users/dossier_prefill_post_spec.rb @@ -142,9 +142,15 @@ describe 'Prefilling a dossier (with a POST request):', js: true do allow(FranceConnectService).to receive(:retrieve_user_informations_particulier).and_return(build(:france_connect_information)) page.find('.fr-connect').click + expect(page).to have_content("Choisissez votre e-mail de contact") + expect(page).to have_selector("#use_france_connect_email_yes", visible: false, wait: 10) + page.execute_script('document.getElementById("use_france_connect_email_yes").click()') + click_on 'Confirmer' + expect(page).to have_content("Confirmation envoyée") + click_on 'Continuer' expect(page).to have_content('Vous avez un dossier prérempli') - click_on 'Poursuivre mon dossier prérempli' + find('.fr-btn.fr-mb-2w', text: 'Poursuivre mon dossier prérempli', wait: 10).click end end end