diff --git a/app/components/procedure/card/api_entreprise_component/api_entreprise_component.fr.yml b/app/components/procedure/card/api_entreprise_component/api_entreprise_component.fr.yml index 688cb12c0..1dc20806f 100644 --- a/app/components/procedure/card/api_entreprise_component/api_entreprise_component.fr.yml +++ b/app/components/procedure/card/api_entreprise_component/api_entreprise_component.fr.yml @@ -1,3 +1,3 @@ --- fr: - title: Jeton Entreprise + title: Jeton API Entreprise diff --git a/app/components/procedure/card/api_entreprise_component/api_entreprise_component.html.haml b/app/components/procedure/card/api_entreprise_component/api_entreprise_component.html.haml index 8c5ed09f9..e7eda1d7c 100644 --- a/app/components/procedure/card/api_entreprise_component/api_entreprise_component.html.haml +++ b/app/components/procedure/card/api_entreprise_component/api_entreprise_component.html.haml @@ -1,11 +1,14 @@ .fr-col-6.fr-col-md-4.fr-col-lg-3 = link_to jeton_admin_procedure_path(@procedure), class: 'fr-tile fr-enlarge-link' do .fr-tile__body.flex.column.align-center.justify-between - - if @procedure.api_entreprise_token.present? - %p.fr-badge.fr-badge--success Validé + - if @procedure.has_api_entreprise_token? + - if @procedure.api_entreprise_token_expired_or_expires_soon? + %p.fr-badge.fr-badge--error À renouveler + - else + %p.fr-badge.fr-badge--success Validé - else %p.fr-badge.fr-badge--info À configurer %div %h3.fr-h6.fr-mt-10v= t('.title') - %p.fr-tile-subtitle Configurer le jeton API entreprise + %p.fr-tile-subtitle Configurer le jeton API Entreprise %p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit') diff --git a/app/models/api_entreprise_token.rb b/app/models/api_entreprise_token.rb index 65cbfa491..b3c1e5b7b 100644 --- a/app/models/api_entreprise_token.rb +++ b/app/models/api_entreprise_token.rb @@ -17,6 +17,10 @@ class APIEntrepriseToken decoded_token.key?("exp") && decoded_token["exp"] <= Time.zone.now.to_i end + def expiration + decoded_token.key?("exp") && Time.zone.at(decoded_token["exp"]) + end + def role?(role) roles.include?(role) end diff --git a/app/models/concerns/api_entreprise_token_concern.rb b/app/models/concerns/api_entreprise_token_concern.rb new file mode 100644 index 000000000..3750d35ef --- /dev/null +++ b/app/models/concerns/api_entreprise_token_concern.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module APIEntrepriseTokenConcern + extend ActiveSupport::Concern + + SOON_TO_EXPIRE_DELAY = 1.month + + included do + validates :api_entreprise_token, jwt_token: true, allow_blank: true + + before_save :set_api_entreprise_token_expires_at, if: :will_save_change_to_api_entreprise_token? + + def api_entreprise_role?(role) + APIEntrepriseToken.new(api_entreprise_token).role?(role) + end + + def api_entreprise_token + self[:api_entreprise_token].presence || Rails.application.secrets.api_entreprise[:key] + end + + def api_entreprise_token_expired_or_expires_soon? + api_entreprise_token_expires_at && api_entreprise_token_expires_at <= SOON_TO_EXPIRE_DELAY.from_now + end + + def has_api_entreprise_token? + self[:api_entreprise_token].present? + end + + def set_api_entreprise_token_expires_at + self.api_entreprise_token_expires_at = has_api_entreprise_token? ? APIEntrepriseToken.new(api_entreprise_token).expiration : nil + end + end +end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index e758b087a..437fbd2a7 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Procedure < ApplicationRecord + include APIEntrepriseTokenConcern include ProcedureStatsConcern include EncryptableConcern include InitiationProcedureConcern @@ -284,7 +285,6 @@ class Procedure < ApplicationRecord size: { less_than: LOGO_MAX_SIZE }, if: -> { new_record? || created_at > Date.new(2020, 11, 13) } - validates :api_entreprise_token, jwt_token: true, allow_blank: true validates :api_particulier_token, format: { with: /\A[A-Za-z0-9\-_=.]{15,}\z/ }, allow_blank: true validate :validate_auto_archive_on_in_the_future, if: :will_save_change_to_auto_archive_on? @@ -762,18 +762,6 @@ class Procedure < ApplicationRecord "Procedure;#{id}" end - def api_entreprise_role?(role) - APIEntrepriseToken.new(api_entreprise_token).role?(role) - end - - def api_entreprise_token - self[:api_entreprise_token].presence || Rails.application.secrets.api_entreprise[:key] - end - - def api_entreprise_token_expired? - APIEntrepriseToken.new(api_entreprise_token).expired? - end - def create_new_revision(revision = nil) transaction do new_revision = (revision || draft_revision) diff --git a/app/tasks/maintenance/update_api_entreprise_token_expires_at_task.rb b/app/tasks/maintenance/update_api_entreprise_token_expires_at_task.rb new file mode 100644 index 000000000..206a270ec --- /dev/null +++ b/app/tasks/maintenance/update_api_entreprise_token_expires_at_task.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Maintenance + class UpdateAPIEntrepriseTokenExpiresAtTask < MaintenanceTasks::Task + def collection + Procedure.with_discarded.where.not(api_entreprise_token: nil) + end + + def process(procedure) + procedure.set_api_entreprise_token_expires_at + procedure.save! + end + end +end diff --git a/app/views/administrateurs/_breadcrumbs.html.haml b/app/views/administrateurs/_breadcrumbs.html.haml index d19911f36..9264a128e 100644 --- a/app/views/administrateurs/_breadcrumbs.html.haml +++ b/app/views/administrateurs/_breadcrumbs.html.haml @@ -28,6 +28,11 @@ - elsif @procedure.locked? = link_to commencer_url(@procedure.path), commencer_url(@procedure.path), class: "fr-link" .flex.fr-mt-1w + + - if @procedure.api_entreprise_token_expired_or_expires_soon? + %span.fr-badge.fr-badge--error.fr-mr-1w + = t('to_modify', scope: [:layouts, :breadcrumb]) + %span.fr-badge.fr-badge--success.fr-mr-1w = t('published', scope: [:layouts, :breadcrumb]) = t('since', scope: [:layouts, :breadcrumb], number: @procedure.id, date: l(@procedure.published_at.to_date)) diff --git a/app/views/administrateurs/procedures/_api_entreprise_token_expiration_alert.html.haml b/app/views/administrateurs/procedures/_api_entreprise_token_expiration_alert.html.haml new file mode 100644 index 000000000..d37e14142 --- /dev/null +++ b/app/views/administrateurs/procedures/_api_entreprise_token_expiration_alert.html.haml @@ -0,0 +1,14 @@ +- if procedure.api_entreprise_token_expires_at.present? + - if procedure.api_entreprise_token_expires_at < Time.zone.now + = render Dsfr::AlertComponent.new(state: :error, size: :sm, extra_class_names: 'fr-mb-2w') do |c| + - c.with_body do + %p + Votre jeton API Entreprise est expiré. + Merci de le renouveler. + - elsif procedure.api_entreprise_token_expired_or_expires_soon? + = render Dsfr::AlertComponent.new(state: :warning, size: :sm, extra_class_names: 'fr-mb-2w') do |c| + - c.with_body do + %p + Votre jeton API Entreprise expirera le + = procedure.api_entreprise_token_expires_at.strftime('%d/%m/%Y à %H:%M.') + Merci de le renouveler avant cette date. diff --git a/app/views/administrateurs/procedures/_procedures_list.html.haml b/app/views/administrateurs/procedures/_procedures_list.html.haml index 535c76267..99eecd912 100644 --- a/app/views/administrateurs/procedures/_procedures_list.html.haml +++ b/app/views/administrateurs/procedures/_procedures_list.html.haml @@ -54,11 +54,15 @@ .text-right %p.fr-mb-0.width-max-content N° #{number_with_html_delimiter(procedure.id)} + - if procedure.close? || procedure.depubliee? %span.fr-badge.fr-badge--sm.fr-badge--warning = t('closed', scope: [:layouts, :breadcrumb]) - elsif procedure.publiee? + - if procedure.api_entreprise_token_expired_or_expires_soon? + %span.fr-badge.fr-badge--sm.fr-badge--error + = t('to_modify', scope: [:layouts, :breadcrumb]) %span.fr-badge.fr-badge--sm.fr-badge--success = t('published', scope: [:layouts, :breadcrumb]) diff --git a/app/views/administrateurs/procedures/jeton.html.haml b/app/views/administrateurs/procedures/jeton.html.haml index 35d92ab90..8172136d5 100644 --- a/app/views/administrateurs/procedures/jeton.html.haml +++ b/app/views/administrateurs/procedures/jeton.html.haml @@ -1,10 +1,10 @@ = render partial: 'administrateurs/breadcrumbs', locals: { steps: [['Démarches', admin_procedures_back_path(@procedure)], [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)], - ['Jeton Entreprise']] } + ['Jeton API Entreprise']] } .fr-container - %h1.fr-h2 Jeton Entreprise + %h1.fr-h2 Jeton API Entreprise = form_with model: @procedure, url: url_for({ controller: 'administrateurs/procedures', action: :update_jeton }) do |f| .fr-container @@ -14,12 +14,16 @@ Démarches Simplifiées utilise = link_to 'API Entreprise', "https://entreprise.api.gouv.fr/" qui permet de récupérer les informations administratives des entreprises et des associations. - Si votre démarche nécessite des autorisations spécifiques que Démarches Simplifiées n’a pas par défaut, merci de renseigner ici le jeton - = link_to 'API Entreprise', "https://api.gouv.fr/les-api/api-entreprise/demande-acces" + Si votre démarche nécessite des autorisations spécifiques que Démarches Simplifiées n’a pas par défaut, merci de renseigner ci-dessous + %strong le jeton API Entreprise propre à votre démarche. + %p + Si besoin, vous pouvez demander une habilitation API Entreprise en cliquant sur le lien suivant : + = link_to "https://api.gouv.fr/les-api/api-entreprise/demande-acces.", "https://api.gouv.fr/les-api/api-entreprise/demande-acces" - .fr-input-group - = f.label :api_entreprise_token, "Jeton", class: 'fr-label' - = f.password_field :api_entreprise_token, value: @procedure.read_attribute(:api_entreprise_token), class: 'fr-input' + + = render partial: 'administrateurs/procedures/api_entreprise_token_expiration_alert', locals: { procedure: @procedure } + + = render Dsfr::InputComponent.new(form: f, attribute: :api_entreprise_token, input_type: :password_field, required: false, opts: { value: @procedure.read_attribute(:api_entreprise_token)}) = render Procedure::FixedFooterComponent.new(procedure: @procedure, form: f) diff --git a/app/views/administrateurs/procedures/show.html.haml b/app/views/administrateurs/procedures/show.html.haml index 8cdfc01c2..4414f921f 100644 --- a/app/views/administrateurs/procedures/show.html.haml +++ b/app/views/administrateurs/procedures/show.html.haml @@ -27,6 +27,15 @@ = link_to 'Clore', admin_procedure_close_path(procedure_id: @procedure.id), class: 'fr-btn fr-btn--tertiary fr-btn--icon-left fr-icon-calendar-close-fill', id: "close-procedure-link" .fr-container + - if @procedure.api_entreprise_token_expired_or_expires_soon? + = render Dsfr::AlertComponent.new(state: :error, title: t(:technical_issues, scope: [:administrateurs, :procedures]), extra_class_names: 'fr-mb-2w') do |c| + - c.with_body do + %ul.fr-mb-0 + %li + Le + = link_to "Jeton API Entreprise", jeton_admin_procedure_path(@procedure), class: 'error-anchor' + est expiré ou va expirer prochainement + - if @procedure.draft_changed? = render Dsfr::CalloutComponent.new(title: t(:has_changes, scope: [:administrateurs, :revision_changes]), icon: "fr-fi-information-line") do |c| - c.with_body do diff --git a/config/locales/models/procedure/en.yml b/config/locales/models/procedure/en.yml index 1328196b5..98d27a1f4 100644 --- a/config/locales/models/procedure/en.yml +++ b/config/locales/models/procedure/en.yml @@ -7,6 +7,7 @@ en: attributes: procedure: hints: + api_entreprise_token: 'For example: eyJhbGciOiJIUzI1NiJ9.eyJ1...' description: Describe in a few lines the context, the aim etc. description_target_audience: Describe in a few lines the final recipients of the process, the eligibility criteria if there are any, the prerequisites, etc. description_pj: Describe the required attachments list if there is any @@ -48,6 +49,7 @@ en: personne_morale: 'Legal entity' declarative_with_state/en_instruction: Instruction declarative_with_state/accepte: Accepted + api_entreprise_token: Token API Entreprise api_particulier_token: Token API Particulier initiated_mail: File sorted for processing notification email received_mail: File submitted notification email diff --git a/config/locales/models/procedure/fr.yml b/config/locales/models/procedure/fr.yml index 35713d133..9cc851e1f 100644 --- a/config/locales/models/procedure/fr.yml +++ b/config/locales/models/procedure/fr.yml @@ -7,6 +7,7 @@ fr: attributes: procedure: hints: + api_entreprise_token: 'Exemple : eyJhbGciOiJIUzI1NiJ9.eyJ1...' description: Décrivez en quelques lignes le contexte, la finalité, etc. description_target_audience: Décrivez en quelques lignes les destinataires finaux de la démarche, les conditions d’éligibilité s’il y en a, les pré-requis, etc. description_pj: Décrivez la liste des pièces jointes à fournir s’il y en a @@ -54,6 +55,7 @@ fr: personne_morale: 'Personne morale' declarative_with_state/en_instruction: En instruction declarative_with_state/accepte: Accepté + api_entreprise_token: Jeton API Entreprise api_particulier_token: Jeton API Particulier initiated_mail: L’email de notification de passage de dossier en instruction received_mail: L’email de notification de dépôt de dossier diff --git a/config/locales/views/administrateurs/procedures/en.yml b/config/locales/views/administrateurs/procedures/en.yml index c1776e841..2f471930d 100644 --- a/config/locales/views/administrateurs/procedures/en.yml +++ b/config/locales/views/administrateurs/procedures/en.yml @@ -67,6 +67,7 @@ en: dpd_part_4: How to do ? You can either send him the link to the procedure on test stage by email, or name him "administrator". In any case, publish your approach only after having had his opinion. back_to_procedure: 'Cancel and return to the procedure page' submit: Publish + technical_issues: "Issues are affecting the proper functioning of the process" check_path: path_not_available: owner: This URL is identical to another of your published procedures. If you publish this procedure, the old one will be unpublished and will no longer be accessible to the public. diff --git a/config/locales/views/administrateurs/procedures/fr.yml b/config/locales/views/administrateurs/procedures/fr.yml index f86a93276..036a90daa 100644 --- a/config/locales/views/administrateurs/procedures/fr.yml +++ b/config/locales/views/administrateurs/procedures/fr.yml @@ -67,6 +67,7 @@ fr: dpd_part_4: Comment faire ? Vous pouvez soit lui communiquer par email le lien vers la démarche en test, ou bien le nommer « administrateur ». Dans tous les cas, ne publiez votre démarche qu’après avoir eu son avis. back_to_procedure: 'Annuler et revenir à la page de la démarche' submit: Publier + technical_issues: Des problèmes impactent le bon fonctionnement de la démarche check_path: path_not_available: owner: Cette url est identique à celle d’une autre de vos démarches publiées. Si vous publiez cette démarche, l’ancienne sera dépubliée et ne sera plus accessible au public. diff --git a/config/locales/views/layouts/_breadcrumb.en.yml b/config/locales/views/layouts/_breadcrumb.en.yml index f0a4ff07d..129028235 100644 --- a/config/locales/views/layouts/_breadcrumb.en.yml +++ b/config/locales/views/layouts/_breadcrumb.en.yml @@ -11,6 +11,7 @@ en: closed: "Closed" published: "Published" draft: "Draft" + to_modify: "To modify" more_info_on_test: "For more information on test stage" go_to_FAQ: "read FAQ" url_FAQ: "/faq#accordion-administrateur-2" diff --git a/config/locales/views/layouts/_breadcrumb.fr.yml b/config/locales/views/layouts/_breadcrumb.fr.yml index 2cb0ed409..cc60f2133 100644 --- a/config/locales/views/layouts/_breadcrumb.fr.yml +++ b/config/locales/views/layouts/_breadcrumb.fr.yml @@ -11,6 +11,7 @@ fr: closed: "Close" published: "Publiée" draft: "En test" + to_modify: "À modifier" more_info_on_test: "Pour plus d’information sur la phase de test" go_to_FAQ: "consulter la FAQ" url_FAQ: "/faq#accordion-administrateur-2" diff --git a/db/migrate/20240924112458_add_api_entreprise_token_expires_at_to_procedures.rb b/db/migrate/20240924112458_add_api_entreprise_token_expires_at_to_procedures.rb new file mode 100644 index 000000000..d7724cd85 --- /dev/null +++ b/db/migrate/20240924112458_add_api_entreprise_token_expires_at_to_procedures.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddAPIEntrepriseTokenExpiresAtToProcedures < ActiveRecord::Migration[7.0] + def change + add_column :procedures, :api_entreprise_token_expires_at, :datetime, precision: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 76e7f27c5..30d626290 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -242,8 +242,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_29_141825) do t.integer "dossier_count" t.string "dossier_state" t.bigint "instructeur_id", null: false - t.datetime "sent_at", precision: nil, null: false t.bigint "procedure_id" + t.datetime "sent_at", precision: nil, null: false t.datetime "updated_at", null: false end @@ -933,6 +933,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_29_141825) do t.boolean "allow_expert_messaging", default: true, null: false t.boolean "allow_expert_review", default: true, null: false t.string "api_entreprise_token" + t.datetime "api_entreprise_token_expires_at", precision: nil t.text "api_particulier_scopes", default: [], array: true t.jsonb "api_particulier_sources", default: {} t.boolean "ask_birthday", default: false, null: false diff --git a/spec/components/procedure/card/api_entreprise_component_spec.rb b/spec/components/procedure/card/api_entreprise_component_spec.rb new file mode 100644 index 000000000..382d425ad --- /dev/null +++ b/spec/components/procedure/card/api_entreprise_component_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Procedure::Card::APIEntrepriseComponent, type: :component do + subject { render_inline(described_class.new(procedure:)) } + + let(:procedure) { create(:procedure, api_entreprise_token:) } + + context "Token is not configured" do + let(:api_entreprise_token) { nil } + + it { is_expected.to have_css('p.fr-badge.fr-badge--info', text: "À configurer") } + end + + context "Token expires soon" do + let(:api_entreprise_token) { JWT.encode({ exp: 2.days.from_now.to_i }, nil, "none") } + + it { is_expected.to have_css('p.fr-badge.fr-badge--error', text: "À renouveler") } + end + + context "Token expires in a long time" do + let(:api_entreprise_token) { JWT.encode({ exp: 2.months.from_now.to_i }, nil, "none") } + + it { is_expected.to have_css('p.fr-badge.fr-badge--success', text: "Validé") } + end +end diff --git a/spec/models/api_entreprise_token_spec.rb b/spec/models/api_entreprise_token_spec.rb index 114d88f8b..394153413 100644 --- a/spec/models/api_entreprise_token_spec.rb +++ b/spec/models/api_entreprise_token_spec.rb @@ -138,4 +138,34 @@ describe APIEntrepriseToken, type: :model do end end end + + describe "#expiration" do + subject { api_entreprise_token.expiration } + + context "without token" do + let(:token) { nil } + + it { expect { subject }.to raise_exception(APIEntrepriseToken::TokenError) } + end + + context "with a blank token" do + let(:token) { "" } + + it { expect { subject }.to raise_exception(APIEntrepriseToken::TokenError) } + end + + context "with an invalid token" do + let(:token) { "NOT-A-VALID-TOKEN" } + + it { expect { subject }.to raise_exception(APIEntrepriseToken::TokenError) } + end + + context "with a valid token" do + let(:token) { "eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiI2NjRkZWEyMS02YWFlLTQwZmYtYWM0Mi1kZmQ3ZGE4YjQ3NmUiLCJqdGkiOiJhcGktZW50cmVwcmlzZS1zdGFnaW5nIiwicm9sZXMiOlsiY2VydGlmaWNhdF9jbmV0cCIsInByb2J0cCIsImV0YWJsaXNzZW1lbnRzIiwicHJpdmlsZWdlcyIsInVwdGltZSIsImF0dGVzdGF0aW9uc19hZ2VmaXBoIiwiYWN0ZXNfaW5waSIsImJpbGFuc19pbnBpIiwiYWlkZXNfY292aWRfZWZmZWN0aWZzIiwiY2VydGlmaWNhdF9yZ2VfYWRlbWUiLCJhdHRlc3RhdGlvbnNfc29jaWFsZXMiLCJlbnRyZXByaXNlX2FydGlzYW5hbGUiLCJmbnRwX2NhcnRlX3BybyIsImNvbnZlbnRpb25zX2NvbGxlY3RpdmVzIiwiZXh0cmFpdHNfcmNzIiwiZXh0cmFpdF9jb3VydF9pbnBpIiwiY2VydGlmaWNhdF9hZ2VuY2VfYmlvIiwibXNhX2NvdGlzYXRpb25zIiwiZG9jdW1lbnRzX2Fzc29jaWF0aW9uIiwiZW9yaV9kb3VhbmVzIiwiYXNzb2NpYXRpb25zIiwiYmlsYW5zX2VudHJlcHJpc2VfYmRmIiwiZW50cmVwcmlzZXMiLCJxdWFsaWJhdCIsImNlcnRpZmljYXRfb3BxaWJpIiwiZW50cmVwcmlzZSIsImV0YWJsaXNzZW1lbnQiXSwic3ViIjoic3RhZ2luZyBkZXZlbG9wbWVudCIsImlhdCI6MTY0MTMwNDcxNCwidmVyc2lvbiI6IjEuMCIsImV4cCI6MTY4ODQ3NTUxNH0.xID66pIlMnBR5_6nG-GidFBzK4Tuuy5ZsWfkMEVB_Ek" } + + it "returns the correct expiration time" do + expect(subject).to eq(Time.zone.at(1688475514)) + end + end + end end diff --git a/spec/models/concerns/api_entreprise_token_concern_spec.rb b/spec/models/concerns/api_entreprise_token_concern_spec.rb new file mode 100644 index 000000000..ce58d2b15 --- /dev/null +++ b/spec/models/concerns/api_entreprise_token_concern_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +describe APIEntrepriseTokenConcern do + describe "#api_entreprise_token_expired_or_expires_soon?" do + subject { procedure.api_entreprise_token_expired_or_expires_soon? } + + let(:procedure) { create(:procedure, api_entreprise_token:) } + + context "when there is no token" do + let(:api_entreprise_token) { nil } + + it { is_expected.to be_falsey } + end + + context "when the token expires in 2 months" do + let(:api_entreprise_token) { JWT.encode({ exp: 2.months.from_now.to_i }, nil, "none") } + + it { is_expected.to be_falsey } + end + + context "when the token expires tomorrow" do + let(:api_entreprise_token) { JWT.encode({ exp: 1.day.from_now.to_i }, nil, "none") } + + it { is_expected.to be_truthy } + end + + context "when the token is expired" do + let(:api_entreprise_token) { JWT.encode({ exp: 1.day.ago.to_i }, nil, "none") } + + it { is_expected.to be_truthy } + end + end + + describe '#set_api_entreprise_token_expires_at (before_save)' do + let(:procedure) { create(:procedure, api_entreprise_token: initial_api_entreprise_token) } + + before do + procedure.api_entreprise_token = api_entreprise_token + end + + subject { procedure.save } + + context "when procedure had no api_entreprise_token" do + let(:initial_api_entreprise_token) { nil } + + context 'when the api_entreprise_token is nil' do + let(:api_entreprise_token) { nil } + + it 'does not set the api_entreprise_token_expires_at' do + expect { subject }.not_to change { procedure.api_entreprise_token_expires_at }.from(nil) + end + end + + context 'when the api_entreprise_token is not valid' do + let(:api_entreprise_token) { "not a token" } + + it do + expect { subject }.not_to change { procedure.api_entreprise_token_expires_at }.from(nil) + end + end + + context 'when the api_entreprise_token is valid' do + let(:expiration_date) { Time.zone.now.beginning_of_minute } + let(:api_entreprise_token) { JWT.encode({ exp: expiration_date.to_i }, nil, 'none') } + + it do + expect { subject }.to change { procedure.api_entreprise_token_expires_at }.from(nil).to(expiration_date) + end + end + end + + context "when procedure had an api_entreprise_token" do + let(:initial_api_entreprise_token) { JWT.encode({ exp: 2.months.from_now.to_i }, nil, "none") } + + context 'when the api_entreprise_token is set to nil' do + let(:api_entreprise_token) { nil } + + it do + expect { subject }.to change { procedure.api_entreprise_token_expires_at }.to(nil) + end + end + end + end +end diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index 527b19d52..a5adf5280 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -621,31 +621,6 @@ describe Procedure do end end - describe 'api_entreprise_token_expired?' do - let(:token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" } - let(:procedure) { create(:procedure, api_entreprise_token: token) } - let(:payload) { - [ - { "exp" => expiration_time } - ] - } - let(:subject) { procedure.api_entreprise_token_expired? } - - before do - allow(JWT).to receive(:decode).with(token, nil, false).and_return(payload) - end - - context "with token expired" do - let(:expiration_time) { (1.day.ago).to_i } - it { is_expected.to be_truthy } - end - - context "with token not expired" do - let(:expiration_time) { (1.day.from_now).to_i } - it { is_expected.to be_falsey } - end - end - describe 'clone' do let(:service) { create(:service) } let(:procedure) do diff --git a/spec/tasks/maintenance/update_api_entreprise_token_expires_at_task_spec.rb b/spec/tasks/maintenance/update_api_entreprise_token_expires_at_task_spec.rb new file mode 100644 index 000000000..4fca851f8 --- /dev/null +++ b/spec/tasks/maintenance/update_api_entreprise_token_expires_at_task_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Maintenance + RSpec.describe UpdateAPIEntrepriseTokenExpiresAtTask do + describe '#collection' do + subject(:collection) { described_class.collection } + + let!(:procedures_with_token) { create_list(:procedure, 3, api_entreprise_token: JWT.encode({}, nil, 'none')) } + let!(:procedure_without_token) { create(:procedure, api_entreprise_token: nil) } + + it 'returns procedures with api_entreprise_token present' do + expect(collection).to match_array(procedures_with_token) + + expect(collection).not_to include(procedure_without_token) + end + end + + describe "#process" do + subject(:process) { described_class.process(procedure) } + + let(:expiration) { 1.month.from_now.beginning_of_minute } + let(:procedure) { create(:procedure) } + + before do + procedure.update_column(:api_entreprise_token, JWT.encode({ exp: expiration.to_i }, nil, "none")) + end + + it do + expect { process }.to change { procedure.reload.api_entreprise_token_expires_at }.from(nil).to(expiration) + end + end + end +end diff --git a/spec/views/administrateurs/procedures/_api_entreprise_token_expiration_alert.html.haml_spec.rb b/spec/views/administrateurs/procedures/_api_entreprise_token_expiration_alert.html.haml_spec.rb new file mode 100644 index 000000000..7cc4b8824 --- /dev/null +++ b/spec/views/administrateurs/procedures/_api_entreprise_token_expiration_alert.html.haml_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +RSpec.describe 'administrateurs/procedures/_api_entreprise_token_expiration_alert', type: :view do + let(:procedure) { create(:procedure, api_entreprise_token:) } + + subject { render 'administrateurs/procedures/api_entreprise_token_expiration_alert', procedure: procedure } + + context "when there is no token" do + let(:api_entreprise_token) { nil } + + it "does not render anything" do + subject + expect(rendered).to be_empty + end + end + + context "when the token is expired" do + let(:api_entreprise_token) { JWT.encode({ exp: 2.days.ago.to_i }, nil, "none") } + + it "should display an error" do + subject + + expect(rendered).to have_content("Votre jeton API Entreprise est expiré") + end + end + + context "when the token expires in few days it should display the expiration date" do + let(:expiration) { 2.days.from_now } + let(:api_entreprise_token) { JWT.encode({ exp: expiration.to_i }, nil, "none") } + + it "should display an error" do + subject + + expect(rendered).to have_content("Votre jeton API Entreprise expirera le\n#{expiration.strftime('%d/%m/%Y à %H:%M')}") + end + end + + context "when the token expires in a long time" do + let(:expiration) { 2.months.from_now } + let(:api_entreprise_token) { JWT.encode({ exp: expiration.to_i }, nil, "none") } + + it "does not render anything" do + subject + expect(rendered).to be_empty + end + end +end