diff --git a/app/assets/images/dsfr/artwork/background/ovoid.svg b/app/assets/images/dsfr/artwork/background/ovoid.svg new file mode 100644 index 000000000..98d816a1f --- /dev/null +++ b/app/assets/images/dsfr/artwork/background/ovoid.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/app/assets/images/dsfr/artwork/pictograms/system/technical-error.svg b/app/assets/images/dsfr/artwork/pictograms/system/technical-error.svg new file mode 100644 index 000000000..3c90712e8 --- /dev/null +++ b/app/assets/images/dsfr/artwork/pictograms/system/technical-error.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index aa3c08696..7584990cb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -419,6 +419,17 @@ class ApplicationController < ActionController::Base prepend_view_path "app/custom_views" end + def try_nav_bar_profile_from_referrer + # detect context from referer, simple (no detection when refreshing the page) + params = Rails.application.routes.recognize_path(request&.referer) + + controller_class = "#{params[:controller].camelize}Controller".safe_constantize + return if controller_class.nil? + + controller_instance = controller_class.new + controller_instance.try(:nav_bar_profile) + end + # Extract a value from params based on the "path" # # params: { dossiers: { champs_public_attributes: { 1234 => { value: "hello" } } } } diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb new file mode 100644 index 000000000..be1b68565 --- /dev/null +++ b/app/controllers/errors_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class ErrorsController < ApplicationController + def nav_bar_profile = try_nav_bar_profile_from_referrer + + rescue_from Exception do + # catch any error, except errors triggered by middlewares outside controller (like warden middleware) + render file: Rails.public_path.join('500.html'), layout: false, status: :internal_server_error + end + + def internal_server_error + # This dynamic template is rendered when a "normal" error occurs, (ie. a bug which is 99.99% of errors.) + # However if this action fails (error in the view or in a middlewares) + # the exceptions are rescued and a basic 100% static html file is rendererd instead. + render_error 500 + end + + def not_found = render_error 404 + def unprocessable_entity = render_error 422 + + def show # generic page for others errors + @status = params[:status].to_i + @error_name = Rack::Utils::HTTP_STATUS_CODES[@status] + + render_error @status + end + + private + + def render_error(status) + respond_to do |format| + format.html { render status: } + format.json { render status:, json: { status:, name: Rack::Utils::HTTP_STATUS_CODES[status] } } + end + end + + # Intercept errors in before_action when fetching user or roles + # when db is unreachable so we can still display a nice 500 static page + def current_user + super + rescue + nil + end + + def current_user_roles + super + rescue + nil + end +end diff --git a/app/controllers/release_notes_controller.rb b/app/controllers/release_notes_controller.rb index 79776bcac..51a4430e0 100644 --- a/app/controllers/release_notes_controller.rb +++ b/app/controllers/release_notes_controller.rb @@ -21,16 +21,7 @@ class ReleaseNotesController < ApplicationController render "scrollable_list" if params[:page].present? end - def nav_bar_profile - # detect context from referer, simple (no detection when refreshing the page) - params = Rails.application.routes.recognize_path(request&.referer) - - controller_class = "#{params[:controller].camelize}Controller".safe_constantize - return if controller_class.nil? - - controller_instance = controller_class.new - controller_instance.try(:nav_bar_profile) - end + def nav_bar_profile = try_nav_bar_profile_from_referrer private diff --git a/app/views/errors/_artwork.html.haml b/app/views/errors/_artwork.html.haml new file mode 100644 index 000000000..c7cd1c5bc --- /dev/null +++ b/app/views/errors/_artwork.html.haml @@ -0,0 +1,9 @@ +.fr-col-12.fr-col-md-3.fr-col-offset-md-1.fr-px-6w.fr-px-md-0.fr-py-0 + %svg.fr-responsive-img.fr-artwork{ xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", width: "160", height: "200", viewBox: "0 0 160 200" } + %use.fr-artwork-motif{ href: image_path("dsfr/artwork/background/ovoid.svg#artwork-motif") } + + %use.fr-artwork-background{ href: image_path('dsfr/artwork/background/ovoid.svg#artwork-background') } + %g{ transform: "translate(40, 60)" } + %use.fr-artwork-decorative{ href: image_path('dsfr/artwork/pictograms/system/technical-error.svg#artwork-decorative') } + %use.fr-artwork-minor{ href: image_path('dsfr/artwork/pictograms/system/technical-error.svg#artwork-minor') } + %use.fr-artwork-major{ href: image_path('dsfr/artwork/pictograms/system/technical-error.svg#artwork-major') } diff --git a/app/views/errors/internal_server_error.en.html.haml b/app/views/errors/internal_server_error.en.html.haml new file mode 100644 index 000000000..5caa4933c --- /dev/null +++ b/app/views/errors/internal_server_error.en.html.haml @@ -0,0 +1,19 @@ +%main#content{ role: "main" } + .fr-container + .fr-my-7w.fr-mt-md-12w.fr-mb-md-10w.fr-grid-row.fr-grid-row--gutters.fr-grid-row--middle.fr-grid-row--center + .fr-py-0.fr-col-12.fr-col-md-6 + %h1 Unexpected Error + %p.fr-text--sm.fr-mb-3w Error 500 + %p.fr-text--lead.fr-mb-3w + Sorry, an error has occurred. Our teams have been notified + to resolve the issue as quickly as possible. + %p.fr-text--sm.fr-mb-5w + Try refreshing the page or try again a little later. + %br + If you need immediate assistance, please contact us. + + %ul.fr-btns-group.fr-btns-group--inline-md + %li + = link_to("Contact Us", contact_path, class: "fr-btn fr-btn--secondary") + + = render partial: "artwork" diff --git a/app/views/errors/internal_server_error.fr.html.haml b/app/views/errors/internal_server_error.fr.html.haml new file mode 100644 index 000000000..e500608e0 --- /dev/null +++ b/app/views/errors/internal_server_error.fr.html.haml @@ -0,0 +1,19 @@ +%main#content{ role: "main" } + .fr-container + .fr-my-7w.fr-mt-md-12w.fr-mb-md-10w.fr-grid-row.fr-grid-row--gutters.fr-grid-row--middle.fr-grid-row--center + .fr-py-0.fr-col-12.fr-col-md-6 + %h1 Erreur inattendue + %p.fr-text--sm.fr-mb-3w Erreur 500 + %p.fr-text--lead.fr-mb-3w + Désolé, une erreur est survenue. Nos équipes ont été averties + pour résoudre le problème le plus rapidement possible. + %p.fr-text--sm.fr-mb-5w + Essayez de rafraîchir la page ou réessayez un peu plus tard. + %br + Si le problème persiste, merci de nous contacter. + + %ul.fr-btns-group.fr-btns-group--inline-md + %li + = link_to("Contactez-nous", contact_path, class: "fr-btn fr-btn--secondary") + + = render partial: "artwork" diff --git a/app/views/errors/not_found.en.html.haml b/app/views/errors/not_found.en.html.haml new file mode 100644 index 000000000..2472750b6 --- /dev/null +++ b/app/views/errors/not_found.en.html.haml @@ -0,0 +1,20 @@ +%main#content{ role: "main" } + .fr-container + .fr-my-7w.fr-mt-md-12w.fr-mb-md-10w.fr-grid-row.fr-grid-row--gutters.fr-grid-row--middle.fr-grid-row--center + .fr-py-0.fr-col-12.fr-col-md-6 + %h1 Page not found + %p.fr-text--sm.fr-mb-3w Error 404 + %p.fr-text--lead.fr-mb-3w The page you are looking for cannot be found. We apologize for the inconvenience. + %p.fr-text--sm.fr-mb-5w + If you typed the web address in the browser, check that it is correct. The page may no longer be available. + %br + In this case, to continue your visit you can check our homepage. + %br + Otherwise, contact us so we can direct you to the correct information. + %ul.fr-btns-group.fr-btns-group--inline-md + %li + = link_to("Homepage", root_path, class: "fr-btn") + %li + = link_to("Contact us", contact_path, class: "fr-btn fr-btn--secondary") + + = render partial: "artwork" diff --git a/app/views/errors/not_found.fr.html.haml b/app/views/errors/not_found.fr.html.haml new file mode 100644 index 000000000..33b39584c --- /dev/null +++ b/app/views/errors/not_found.fr.html.haml @@ -0,0 +1,22 @@ +%main#content{ role: "main" } + .fr-container + .fr-my-7w.fr-mt-md-12w.fr-mb-md-10w.fr-grid-row.fr-grid-row--gutters.fr-grid-row--middle.fr-grid-row--center + .fr-py-0.fr-col-12.fr-col-md-6 + %h1 Page non trouvée + %p.fr-text--sm.fr-mb-3w Erreur 404 + %p.fr-text--lead.fr-mb-3w La page que vous cherchez est introuvable. Excusez-nous pour la gène occasionnée. + %p.fr-text--sm.fr-mb-3w + Si vous avez tapé l’adresse web dans le navigateur, vérifiez qu’elle est correcte. La page n’est peut-être plus disponible. + %br + Dans ce cas, pour continuer votre visite vous pouvez consulter notre page d’accueil. + %br + Sinon contactez-nous pour que l’on puisse vous rediriger vers la bonne information. + %p.fr-text--sm.fr-mb-5w + Pour le laissez-passer A-38, relatif à l’enregistrement d'une galère, veuillez vous adresser à la capitainerie au port. + %ul.fr-btns-group.fr-btns-group--inline-md + %li + = link_to("Page d’accueil", root_path, class: "fr-btn") + %li + = link_to("Contactez-nous", contact_path, class: "fr-btn fr-btn--secondary") + + = render partial: "artwork" diff --git a/app/views/errors/show.en.html.haml b/app/views/errors/show.en.html.haml new file mode 100644 index 000000000..93b97a353 --- /dev/null +++ b/app/views/errors/show.en.html.haml @@ -0,0 +1,19 @@ +%main#content{ role: "main" } + .fr-container + .fr-my-7w.fr-mt-md-12w.fr-mb-md-10w.fr-grid-row.fr-grid-row--gutters.fr-grid-row--middle.fr-grid-row--center + .fr-py-0.fr-col-12.fr-col-md-6 + %h1= @error_name + %p.fr-text--sm.fr-mb-3w Error #{@status} + %p.fr-text--lead.fr-mb-3w An error prevents this page from loading. + + - if @error_name.present? # valid error code + %p.fr-text--sm.fr-mb-5w + = link_to("What does that mean?", "https://developer.mozilla.org/en/docs/Web/HTTP/Status/#{@status}", **external_link_attributes) + + %ul.fr-btns-group.fr-btns-group--inline-md + %li + = link_to("Homepage", root_path, class: "fr-btn") + %li + = link_to("Contact us", contact_path, class: "fr-btn fr-btn--secondary") + + = render partial: "artwork" diff --git a/app/views/errors/show.fr.html.haml b/app/views/errors/show.fr.html.haml new file mode 100644 index 000000000..7e607420a --- /dev/null +++ b/app/views/errors/show.fr.html.haml @@ -0,0 +1,19 @@ +%main#content{ role: "main" } + .fr-container + .fr-my-7w.fr-mt-md-12w.fr-mb-md-10w.fr-grid-row.fr-grid-row--gutters.fr-grid-row--middle.fr-grid-row--center + .fr-py-0.fr-col-12.fr-col-md-6 + %h1= @error_name + %p.fr-text--sm.fr-mb-3w Erreur #{@status} + %p.fr-text--lead.fr-mb-3w Une erreur empêche le chargement de cette page. + + - if @error_name.present? # valid error code + %p.fr-text--sm.fr-mb-5w + = link_to("Qu’est-ce que cela veut dire ?", "https://developer.mozilla.org/fr/docs/Web/HTTP/Status/#{@status}", **external_link_attributes) + + %ul.fr-btns-group.fr-btns-group--inline-md + %li + = link_to("Page d’accueil", root_path, class: "fr-btn") + %li + = link_to("Contactez-nous", contact_path, class: "fr-btn fr-btn--secondary") + + = render partial: "artwork" diff --git a/app/views/errors/unprocessable_entity.en.html.haml b/app/views/errors/unprocessable_entity.en.html.haml new file mode 100644 index 000000000..4acd9ba03 --- /dev/null +++ b/app/views/errors/unprocessable_entity.en.html.haml @@ -0,0 +1,24 @@ +%main#content{ role: "main" } + .fr-container + .fr-my-7w.fr-mt-md-12w.fr-mb-md-10w.fr-grid-row.fr-grid-row--gutters.fr-grid-row--middle.fr-grid-row--center + .fr-py-0.fr-col-12.fr-col-md-6 + %h1 The requested action has been rejected + %p.fr-text--sm.fr-mb-3w Error 422 + %p.fr-text--lead.fr-mb-3w We're sorry, but we can't process your request. + %p.fr-text--sm.fr-mb-5w + This may be due to a request that cannot be processed in its current state. + %br + Go back to + = link_to("the previous page", "javascript:window.location = document.referrer", class: "fr-link") + and then try again. + %br + + If the problem persists, please don't hesitate to contact us for assistance. + + %ul.fr-btns-group.fr-btns-group--inline-md + %li + = link_to 'Back', 'javascript:window.location = document.referrer', class: 'fr-btn' + %li + = link_to 'Contact us', contact_path, class: 'fr-btn fr-btn--secondary' + + = render partial: "artwork" diff --git a/app/views/errors/unprocessable_entity.fr.html.haml b/app/views/errors/unprocessable_entity.fr.html.haml new file mode 100644 index 000000000..8653830a8 --- /dev/null +++ b/app/views/errors/unprocessable_entity.fr.html.haml @@ -0,0 +1,24 @@ +%main#content{ role: "main" } + .fr-container + .fr-my-7w.fr-mt-md-12w.fr-mb-md-10w.fr-grid-row.fr-grid-row--gutters.fr-grid-row--middle.fr-grid-row--center + .fr-py-0.fr-col-12.fr-col-md-6 + %h1 L’action demandée a été rejetée + %p.fr-text--sm.fr-mb-3w Erreur 422 + %p.fr-text--lead.fr-mb-3w Nous sommes désolés, mais nous ne pouvons pas traiter votre requête. + %p.fr-text--sm.fr-mb-5w + Cela peut être dû à une requête qui ne peut être traitée dans son état actuel. + %br + Revenez à + = link_to("la page précédente", "javascript:window.location = document.referrer", class: "fr-link") + puis réessayez. + %br + + Si le problème persiste, n’hésitez pas à nous contacter pour obtenir de l’aide. + + %ul.fr-btns-group.fr-btns-group--inline-md + %li + = link_to 'Retour', 'javascript:window.location = document.referrer', class: 'fr-btn' + %li + = link_to 'Contactez-nous', contact_path, class: 'fr-btn fr-btn--secondary' + + = render partial: "artwork" diff --git a/config/application.rb b/config/application.rb index 2e79f8f01..638dfa89e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -104,6 +104,8 @@ module TPS config.active_record.encryption.primary_key = Rails.application.secrets.active_record_encryption.fetch(:primary_key) config.active_record.encryption.key_derivation_salt = Rails.application.secrets.active_record_encryption.fetch(:key_derivation_salt) + config.exceptions_app = self.routes + # Copied from rgeo/activerecord-postgis-adapter ActiveRecord::SchemaDumper.ignore_tables |= [ 'geography_columns', diff --git a/config/routes.rb b/config/routes.rb index bc65dfb59..18317b7f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -698,6 +698,11 @@ Rails.application.routes.draw do resources :release_notes, only: [:index] + get '/404', to: 'errors#not_found' + get '/422', to: 'errors#unprocessable_entity' + get '/500', to: 'errors#internal_server_error' + get '/:status', to: 'errors#show', constraints: { status: /[4-5][0-5]\d/ } + if Rails.env.test? scope 'test/api_geo' do get 'regions' => 'api_geo_test#regions' diff --git a/public/404.html b/public/404.html deleted file mode 100644 index 297300fe2..000000000 --- a/public/404.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - La page que vous cherchez n’existe pas (erreur 404) - - - - - - - -
-
-

La page que vous cherchez n’existe pas (erreur 404).

-

La page que vous cherchez a sans doute changé d’adresse, ou vous n’avez pas les droits nécessaires pour y accéder.

-
-
- - diff --git a/public/404_procedure_not_found.html b/public/404_procedure_not_found.html deleted file mode 100644 index 93661f20b..000000000 --- a/public/404_procedure_not_found.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - The page you were looking for doesn't exist (404) - - - - - - -
-
-

Cette démarche n'existe pas.

-

Merci de vérifier le lien que vous avez suivi et/ou de contacter votre administrateur.

-
-

Si vous êtes l'administrateur de l'application, merci de regarder les logs.

-
- - diff --git a/public/422.html b/public/422.html deleted file mode 100644 index 899adfc29..000000000 --- a/public/422.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - Erreur 422 · demarches-simplifiees.fr - - - - -
-
-
-
- -
-
-
-
-

L’action demandée a été rejetée.

-

- Pas de panique, c’est probablement temporaire. -

-

- Essayez de - recharger la page précédente, - et tout devrait rentrer dans l’ordre. -

-
-
- - diff --git a/public/500.html b/public/500.html index 94047a0d5..778e9f022 100644 --- a/public/500.html +++ b/public/500.html @@ -1,53 +1,2157 @@ - - - - - - - - - Erreur 500 · demarches-simplifiees.fr - - - - -
-
-
-
- + + + + + + + + + Erreur inattendue - demarches-simplifiees.fr + + + + + + +
-
-

Une erreur est survenue

-
- Nos équipes ont été averties. Nous mettons tout en oeuvre pour résoudre ce problème dans les meilleurs délais. + + +
+
+
+
+

Erreur inattendue

+

Erreur 500

+

+ Désolé, une erreur est survenue. Nos équipes ont été averties pour + résoudre le problème le plus rapidement possible. +

+

+ Essayez de rafraîchir la page ou bien ressayez un peu plus tard. +
+ Si vous avez besoin d’une aide immédiate, merci de nous contacter. +

+ +
+
-
-
- + + diff --git a/public/502.html b/public/502.html new file mode 100644 index 000000000..f4bea681f --- /dev/null +++ b/public/502.html @@ -0,0 +1,2156 @@ + + + + + + + + + Problème de connexion au serveur - demarches-simplifiees.fr + + + + + + + + +
+
+
+
+

Problème de connexion au serveur

+

Erreur 502

+

+ Désolé, il semble y avoir un problème de connexion à nos serveurs. + Cela peut être dû à une brève interruption ou une maintenance en cours. +

+

+ Nous vous conseillons de recharger la page. Si le problème + persiste, essayez à nouveau dans quelques instants. + Merci de votre patience. +

+ +
+
+
+
+ + diff --git a/public/503.html b/public/503.html new file mode 100644 index 000000000..5e6dfebb4 --- /dev/null +++ b/public/503.html @@ -0,0 +1,2158 @@ + + + + + + + + + Service temporairement indisponible - demarches-simplifiees.fr + + + + + + + + +
+
+
+
+

Service temporairement indisponible

+

Erreur 503

+

+ Nous effectuons une maintenance ou notre site est + temporairement surchargé. Nous travaillons pour restaurer le + service aussi rapidement que possible. +

+

+ Réessayer dans quelques minutes. Merci pour votre patience. +
+ Si vous avez besoin d’une aide immédiate ou pour plus + d’informations, vous pouvez nous contacter. +

+ +
+
+
+
+ + diff --git a/public/504.html b/public/504.html new file mode 100644 index 000000000..90dd2745d --- /dev/null +++ b/public/504.html @@ -0,0 +1,2166 @@ + + + + + + + + + Délai de connexion dépassé - demarches-simplifiees.fr + + + + + + + + +
+
+
+
+

Délai de connexion dépassé

+

Erreur 504

+

+ Le serveur met trop de temps à répondre. +

+

+ Cela peut être dû à des problèmes temporaires sur nos serveurs, + ou à un problème spécifique à cette page si elle est trop longue à générer. +

+ +

+

+ Essayez de recharger la page dans quelques instants. +
+ Si le problème persiste pour cette page spécifiquement, contactez-nous pour obtenir de l’aide. +

+ +
+
+
+
+ + diff --git a/public/fonts/Marianne-Bold.woff2 b/public/fonts/Marianne-Bold.woff2 new file mode 100755 index 000000000..6ea93fd65 Binary files /dev/null and b/public/fonts/Marianne-Bold.woff2 differ diff --git a/public/fonts/Marianne-Regular.woff2 b/public/fonts/Marianne-Regular.woff2 new file mode 100755 index 000000000..d19d7cc99 Binary files /dev/null and b/public/fonts/Marianne-Regular.woff2 differ diff --git a/spec/controllers/errors_controller_spec.rb b/spec/controllers/errors_controller_spec.rb new file mode 100644 index 000000000..e492086dc --- /dev/null +++ b/spec/controllers/errors_controller_spec.rb @@ -0,0 +1,59 @@ +RSpec.describe ErrorsController, type: :controller do + render_views + + describe 'GET #show' do + # rspec can't easily manage the exceptions_app for a real route, + # just verify the action renders correctly + let(:status_code) { 426 } + let(:status_message) { 'Upgrade Required' } + + context 'HTML format' do + subject do + get :show, params: { status: status_code }, format: :html + end + + it 'correctly handles and responds with an HTML response' do + subject + expect(response).to have_http_status(status_code) + expect(response.body).to include(status_message) + end + end + + context 'JSON format' do + subject do + get :show, params: { status: status_code }, format: :json + end + + it 'correctly handles and responds with a JSON response' do + subject + expect(response).to have_http_status(status_code) + json_response = response.parsed_body + + expect(json_response['status']).to eq(status_code) + expect(json_response['name']).to eq(status_message) + end + end + end + + shared_examples 'specific action' do + subject { get action_name } + + it do + is_expected.to have_http_status(status_code) + end + + context "404" do + let(:status_code) { 404 } + let(:action_name) { :not_found } + + it_behaves_like 'specific action' + end + + context "422" do + let(:status_code) { 422 } + let(:action_name) { :unprocessable_entity } + + it_behaves_like 'specific action' + end + end +end diff --git a/spec/support/without_detailed_exceptions.rb b/spec/support/without_detailed_exceptions.rb new file mode 100644 index 000000000..12e384f11 --- /dev/null +++ b/spec/support/without_detailed_exceptions.rb @@ -0,0 +1,18 @@ +module WithoutDetailedExceptions + RSpec.configure do |config| + config.include self, type: :system + end + + # Snippet from https://github.com/rspec/rspec-rails/issues/2024 + def without_detailed_exceptions + env_config = Rails.application.env_config + original_show_exceptions = env_config['action_dispatch.show_exceptions'] + original_show_detailed_exceptions = env_config['action_dispatch.show_detailed_exceptions'] + env_config['action_dispatch.show_exceptions'] = true + env_config['action_dispatch.show_detailed_exceptions'] = false + yield + ensure + env_config['action_dispatch.show_exceptions'] = original_show_exceptions + env_config['action_dispatch.show_detailed_exceptions'] = original_show_detailed_exceptions + end +end diff --git a/spec/system/errors_spec.rb b/spec/system/errors_spec.rb new file mode 100644 index 000000000..1edd147c0 --- /dev/null +++ b/spec/system/errors_spec.rb @@ -0,0 +1,29 @@ +describe 'Errors handling', js: false do + let(:procedure) { create(:procedure) } + + scenario 'bug renders dynamic 500 page' do + procedure.revisions.destroy_all # break procedure + + without_detailed_exceptions do + visit commencer_path(path: procedure.path) + end + + expect(page).to have_http_status(:internal_server_error) + expect(page).to have_content('une erreur est survenue') + expect(page).to have_content('Se connecter') + expect(page).to have_link('Contactez-nous') + end + + scenario 'fatal error fallback to static 500 page' do + without_detailed_exceptions do + Rails.application.env_config["action_dispatch.cookies"] = "will fail" + visit commencer_path(path: procedure.path) + ensure + Rails.application.env_config.delete("action_dispatch.cookies") + end + + expect(page).to have_content('une erreur est survenue') + expect(page).not_to have_content('Se connecter') + expect(page).to have_link('Contactez-nous') + end +end