From dfd312848dda115a650164c59244aa55b3705ba8 Mon Sep 17 00:00:00 2001 From: Samuel-Zacharie FAURE Date: Tue, 11 Aug 2026 15:43:46 +0200 Subject: [PATCH 1/6] feat: add DataPass API client --- .../clients/abstract_data_pass_api_client.rb | 23 +++++++++ .../clients/data_pass_api_authentication.rb | 21 ++++++++ site/app/clients/data_pass_api_client.rb | 14 ++++++ site/config/initializers/api_base_urls.rb | 10 ++++ .../data_pass_api_authentication_spec.rb | 29 +++++++++++ .../spec/clients/data_pass_api_client_spec.rb | 49 +++++++++++++++++++ 6 files changed, 146 insertions(+) create mode 100644 site/app/clients/abstract_data_pass_api_client.rb create mode 100644 site/app/clients/data_pass_api_authentication.rb create mode 100644 site/app/clients/data_pass_api_client.rb create mode 100644 site/spec/clients/data_pass_api_authentication_spec.rb create mode 100644 site/spec/clients/data_pass_api_client_spec.rb diff --git a/site/app/clients/abstract_data_pass_api_client.rb b/site/app/clients/abstract_data_pass_api_client.rb new file mode 100644 index 0000000000..ec880426e9 --- /dev/null +++ b/site/app/clients/abstract_data_pass_api_client.rb @@ -0,0 +1,23 @@ +require 'faraday' + +class AbstractDataPassAPIClient + protected + + def http_connection(&block) + Faraday.new do |conn| + conn.request :retry, max: 5 + conn.response :raise_error + conn.response :json + conn.options.timeout = 2 + yield(conn) if block + end + end + + def client_id + AdminApientreprise.credentials[:datapass_client_id] + end + + def client_secret + AdminApientreprise.credentials[:datapass_client_secret] + end +end diff --git a/site/app/clients/data_pass_api_authentication.rb b/site/app/clients/data_pass_api_authentication.rb new file mode 100644 index 0000000000..b390cddc80 --- /dev/null +++ b/site/app/clients/data_pass_api_authentication.rb @@ -0,0 +1,21 @@ +# :nocov: +class DataPassAPIAuthentication < AbstractDataPassAPIClient + def access_token + http_connection.post( + auth_url, + URI.encode_www_form( + grant_type: 'client_credentials', + client_id:, + client_secret:, + scope: 'read_authorizations' + ), + 'Content-Type' => 'application/x-www-form-urlencoded' + ).body['access_token'] + end + + private + + def auth_url + "#{DataPass::BASE_URL}/api/oauth/token" + end +end diff --git a/site/app/clients/data_pass_api_client.rb b/site/app/clients/data_pass_api_client.rb new file mode 100644 index 0000000000..995b12f5d4 --- /dev/null +++ b/site/app/clients/data_pass_api_client.rb @@ -0,0 +1,14 @@ +# :nocov: +class DataPassAPIClient < AbstractDataPassAPIClient + def definitions(api) + http_connection.get("#{DataPass::BASE_URL}/api/v1/definitions/#{api}").body + end + + protected + + def http_connection + super do |conn| + conn.request :authorization, 'Bearer', -> { DataPassAPIAuthentication.new.access_token } + end + end +end diff --git a/site/config/initializers/api_base_urls.rb b/site/config/initializers/api_base_urls.rb index 4cda89462d..39dfed0b6c 100644 --- a/site/config/initializers/api_base_urls.rb +++ b/site/config/initializers/api_base_urls.rb @@ -17,3 +17,13 @@ module APIParticulier 'https://particulier.api.gouv.fr' end end + +module DataPass + BASE_URL = if Rails.env.sandbox? + 'https://sandbox.datapass.api.gouv.fr' + elsif Rails.env.staging? || Rails.env.development? + 'https://staging.datapass.api.gouv.fr' + else + 'https://datapass.api.gouv.fr' + end +end diff --git a/site/spec/clients/data_pass_api_authentication_spec.rb b/site/spec/clients/data_pass_api_authentication_spec.rb new file mode 100644 index 0000000000..5a18be6fc8 --- /dev/null +++ b/site/spec/clients/data_pass_api_authentication_spec.rb @@ -0,0 +1,29 @@ +RSpec.describe DataPassAPIAuthentication do + describe '#access_token' do + subject(:access_token) { described_class.new.access_token } + + let(:auth_url) { "#{DataPass::BASE_URL}/api/oauth/token" } + + before do + allow(AdminApientreprise).to receive(:credentials).and_return( + datapass_client_id: 'test_client_id', + datapass_client_secret: 'test_client_secret' + ) + + stub_request(:post, auth_url) + .with( + body: 'grant_type=client_credentials&client_id=test_client_id&client_secret=test_client_secret&scope=read_authorizations', + headers: { 'Content-Type' => 'application/x-www-form-urlencoded' } + ) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { access_token: 'data_pass_access_token' }.to_json + ) + end + + it 'requests a token with the read_authorizations scope' do + expect(access_token).to eq('data_pass_access_token') + end + end +end diff --git a/site/spec/clients/data_pass_api_client_spec.rb b/site/spec/clients/data_pass_api_client_spec.rb new file mode 100644 index 0000000000..7eff0dd12b --- /dev/null +++ b/site/spec/clients/data_pass_api_client_spec.rb @@ -0,0 +1,49 @@ +RSpec.describe DataPassAPIClient do + let(:data_pass_api_authentication) { instance_double(DataPassAPIAuthentication, access_token: 'access_token') } + + before do + allow(DataPassAPIAuthentication).to receive(:new).and_return(data_pass_api_authentication) + end + + describe '#definitions' do + subject(:definitions) { described_class.new.definitions('api_entreprise') } + + let(:url) { "#{DataPass::BASE_URL}/api/v1/definitions/api_entreprise" } + + context 'when the API returns a 200' do + let(:payload) do + { + 'id' => 'api_entreprise', + 'name' => 'API Entreprise', + 'scopes' => [ + { 'name' => 'Data', 'value' => 'a_scope', 'group' => 'Group', 'provider' => 'INSEE', 'link' => nil } + ] + } + end + + before do + stub_request(:get, url) + .with(headers: { 'Authorization' => 'Bearer access_token' }) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: payload.to_json + ) + end + + it 'returns the parsed definition payload' do + expect(definitions).to eq(payload) + end + end + + context 'when the API returns an error status' do + before do + stub_request(:get, url).to_return(status: 500) + end + + it 'raises a Faraday error' do + expect { definitions }.to raise_error(Faraday::Error) + end + end + end +end From 428619665af6735610a429412e02b5280cde7300 Mon Sep 17 00:00:00 2001 From: Samuel-Zacharie FAURE Date: Tue, 11 Aug 2026 15:54:01 +0200 Subject: [PATCH 2/6] test: stub DataPass API by default in feature specs --- site/spec/rails_helper.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/site/spec/rails_helper.rb b/site/spec/rails_helper.rb index 928b20696d..dba92e1b8b 100644 --- a/site/spec/rails_helper.rb +++ b/site/spec/rails_helper.rb @@ -94,6 +94,17 @@ config.before(:each, type: :feature) do stub_request(:get, %r{entreprise\.api\.gouv\.fr/ping}).to_return(status: 200) stub_request(:get, %r{particulier\.api\.gouv\.fr/api/.*/ping$}).to_return(status: 200) + stub_request(:post, "#{DataPass::BASE_URL}/api/oauth/token").to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { access_token: 'test_data_pass_access_token' }.to_json + ) + stub_request(:get, %r{#{Regexp.escape(DataPass::BASE_URL)}/api/v1/definitions/}) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { 'scopes' => [] }.to_json + ) allow(SimplifionsStore.instance).to receive_messages(all: [], for_endpoint: []) end From 1563326f7c4a6a6adba470c34c0010062fc9b990 Mon Sep 17 00:00:00 2001 From: Samuel-Zacharie FAURE Date: Tue, 11 Aug 2026 15:57:08 +0200 Subject: [PATCH 3/6] feat: add ScopeCatalog service --- site/app/services/scope_catalog.rb | 42 ++++++++++++ site/spec/services/scope_catalog_spec.rb | 86 ++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 site/app/services/scope_catalog.rb create mode 100644 site/spec/services/scope_catalog_spec.rb diff --git a/site/app/services/scope_catalog.rb b/site/app/services/scope_catalog.rb new file mode 100644 index 0000000000..8d7d91740a --- /dev/null +++ b/site/app/services/scope_catalog.rb @@ -0,0 +1,42 @@ +class ScopeCatalog + CACHE_TTL = ENV.fetch('DATAPASS_SCOPE_CATALOG_CACHE_TTL_MINUTES', '360').to_i.minutes + + def self.for(api) + new(api) + end + + def initialize(api) + @api = api + end + + def lookup(scope_value) + scopes[scope_value] + end + + private + + attr_reader :api + + def scopes + Rails.cache.fetch(cache_key, expires_in: CACHE_TTL) do + fetch_scopes.tap { |data| Rails.cache.write(stale_cache_key, data, expires_in: nil) } + end + rescue Faraday::Error, TypeError, NoMethodError => e + Sentry.capture_exception(e) + Rails.cache.read(stale_cache_key) || {} + end + + def fetch_scopes + DataPassAPIClient.new.definitions(api)['scopes'].to_h do |scope| + [scope['value'], { provider: scope['provider'], group: scope['group'], name: scope['name'] }] + end + end + + def cache_key + "data_pass_scope_catalog/#{api}" + end + + def stale_cache_key + "#{cache_key}/stale" + end +end diff --git a/site/spec/services/scope_catalog_spec.rb b/site/spec/services/scope_catalog_spec.rb new file mode 100644 index 0000000000..28d03fdc0f --- /dev/null +++ b/site/spec/services/scope_catalog_spec.rb @@ -0,0 +1,86 @@ +require 'rails_helper' + +RSpec.describe ScopeCatalog do + include ActiveSupport::Testing::TimeHelpers + + describe '#lookup' do + subject(:lookup) { described_class.for('api_entreprise').lookup('unites_legales_etablissements_insee') } + + let(:data_pass_api_client) { instance_double(DataPassAPIClient, definitions: definitions_payload) } + let(:definitions_payload) do + { + 'scopes' => [ + { + 'value' => 'unites_legales_etablissements_insee', + 'name' => 'Data', + 'group' => 'Informations générales', + 'provider' => 'INSEE' + } + ] + } + end + + before do + allow(DataPassAPIClient).to receive(:new).and_return(data_pass_api_client) + end + + it 'returns the provider/group/name for a known scope' do + expect(lookup).to eq(provider: 'INSEE', group: 'Informations générales', name: 'Data') + end + + it 'returns nil for an unknown scope' do + expect(described_class.for('api_entreprise').lookup('totally_unknown')).to be_nil + end + + it 'caches the fetched catalog so a second lookup does not call the client again' do + described_class.for('api_entreprise').lookup('unites_legales_etablissements_insee') + described_class.for('api_entreprise').lookup('unites_legales_etablissements_insee') + + expect(data_pass_api_client).to have_received(:definitions).once + end + + it 'writes the stale cache entry without an expiration so it never expires on its own' do + allow(Rails.cache).to receive(:write).and_call_original + + lookup + + expect(Rails.cache).to have_received(:write).with(anything, anything, expires_in: nil) + end + + context 'when DataPass is unreachable and no cache exists yet' do + before do + allow(data_pass_api_client).to receive(:definitions).and_raise(Faraday::TimeoutError) + end + + it 'returns nil instead of raising' do + expect(lookup).to be_nil + end + end + + context 'when DataPass returns an unexpected payload shape and no cache exists yet' do + it 'returns nil instead of raising when the payload is not a Hash' do + allow(data_pass_api_client).to receive(:definitions).and_return([]) + + expect(lookup).to be_nil + end + + it 'returns nil instead of raising when definitions returns nil' do + allow(data_pass_api_client).to receive(:definitions).and_return(nil) + + expect(lookup).to be_nil + end + end + + context 'when DataPass is unreachable but a previous successful fetch was cached' do + before do + described_class.for('api_entreprise').lookup('unites_legales_etablissements_insee') + travel(described_class::CACHE_TTL + 1.minute) + allow(data_pass_api_client).to receive(:definitions).and_raise(Faraday::TimeoutError) + end + + it 'serves the stale cached value instead of raising' do + expect(lookup).to eq(provider: 'INSEE', group: 'Informations générales', name: 'Data') + end + end + end +end From abe30f37d5ade9c3e29f71c034b87b668e9113c6 Mon Sep 17 00:00:00 2001 From: Samuel-Zacharie FAURE Date: Tue, 11 Aug 2026 16:12:56 +0200 Subject: [PATCH 4/6] feat: source scope labels from ScopeCatalog instead of locale files --- site/app/helpers/scope_helper.rb | 16 ++- site/spec/helpers_spec/scope_helper_spec.rb | 133 ++++++++++++++++---- 2 files changed, 123 insertions(+), 26 deletions(-) diff --git a/site/app/helpers/scope_helper.rb b/site/app/helpers/scope_helper.rb index f188d67b03..1db5b9ec4d 100644 --- a/site/app/helpers/scope_helper.rb +++ b/site/app/helpers/scope_helper.rb @@ -1,4 +1,6 @@ module ScopeHelper + UNKNOWN_SCOPE_GROUP = 'Autres'.freeze + def build_scopes(scopes, api) scopes_tree = {} scopes.each do |scope| @@ -9,11 +11,23 @@ def build_scopes(scopes, api) end def humanize_scope(scope, api) - I18n.t("api_#{api}.tokens.token.scope.#{scope}.label", default: scope.humanize) + entry = ScopeCatalog.for(api).lookup(scope) + scope_display_parts(api, entry, scope).join(' || ') end private + def scope_display_parts(api, entry, scope) + name = entry&.dig(:name).presence || scope.humanize + provider = entry&.dig(:provider).presence || UNKNOWN_SCOPE_GROUP + if api == 'api_particulier' + group = entry&.dig(:group).presence || UNKNOWN_SCOPE_GROUP + [provider, group, name] + else + [provider, name] + end + end + def build_scopes_parts(scopes_tree, splitted_scope) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity if splitted_scope.size > 2 scopes_tree[splitted_scope[0]] ||= {} diff --git a/site/spec/helpers_spec/scope_helper_spec.rb b/site/spec/helpers_spec/scope_helper_spec.rb index 222663d918..0b9e2af24d 100644 --- a/site/spec/helpers_spec/scope_helper_spec.rb +++ b/site/spec/helpers_spec/scope_helper_spec.rb @@ -1,44 +1,127 @@ -require 'rails_helper' - RSpec.describe ScopeHelper do include described_class + let(:scope_catalog) { instance_double(ScopeCatalog) } + + before do + allow(ScopeCatalog).to receive(:for).and_return(scope_catalog) + end + describe '#humanize_scope' do - it 'returns the I18n label for a known scope' do - expect(humanize_scope('unites_legales_etablissements_insee', 'entreprise')) - .to eq('INSEE || API Données unités légales et établissements dont les non diffusibles') + before do + allow(scope_catalog).to receive(:lookup).and_return(catalog_entry) + end + + context 'when the api is api_entreprise (2 levels: provider || name)' do + let(:catalog_entry) { { provider: 'INSEE', group: 'Informations générales', name: 'Données unités légales et établissements' } } + + it 'returns provider || name, ignoring group' do + expect(humanize_scope('unites_legales_etablissements_insee', 'api_entreprise')) + .to eq('INSEE || Données unités légales et établissements') + end + end + + context 'when the api is api_particulier (3 levels: provider || group || name)' do + let(:catalog_entry) { { provider: 'CNAF & MSA', group: 'API Quotient familial', name: 'Identités allocataire et conjoint' } } + + it 'returns provider || group || name' do + expect(humanize_scope('cnaf_allocataires', 'api_particulier')) + .to eq('CNAF & MSA || API Quotient familial || Identités allocataire et conjoint') + end end - it 'falls back to scope.humanize when no I18n label is defined' do - expect(humanize_scope('totally_unknown_scope', 'entreprise')) - .to eq('Totally unknown scope') + context 'when the scope is unknown to the catalog' do + let(:catalog_entry) { nil } + + it 'falls back to the "Autres" group with scope.humanize' do + expect(humanize_scope('totally_unknown_scope', 'api_entreprise')) + .to eq('Autres || Totally unknown scope') + end + end + + context 'when the resolved entry has every field blank' do + let(:catalog_entry) { { provider: nil, group: nil, name: nil } } + + it 'still produces a splittable 3-part string for api_particulier' do + expect(humanize_scope('totally_blank_scope', 'api_particulier')) + .to eq('Autres || Autres || Totally blank scope') + end + + it 'still produces a splittable 2-part string for api_entreprise' do + expect(humanize_scope('totally_blank_scope', 'api_entreprise')) + .to eq('Autres || Totally blank scope') + end end end describe '#build_scopes' do - it 'returns correct tree structure for cnaf_quotient_familial scope' do - scopes = ['cnaf_quotient_familial'] + context 'when the api is api_particulier' do + before do + allow(scope_catalog).to receive(:lookup).with('cnaf_quotient_familial').and_return( + provider: 'CNAF & MSA', group: 'API Quotient familial', name: 'Quotient familial CAF & MSA' + ) + allow(scope_catalog).to receive(:lookup).with('cnaf_allocataires').and_return( + provider: 'CNAF & MSA', group: 'API Quotient familial', name: 'Identités allocataire et conjoint' + ) + end + + it 'nests scopes under provider then group' do + result = build_scopes(%w[cnaf_quotient_familial cnaf_allocataires], 'api_particulier') + + expect(result).to eq({ + 'CNAF & MSA' => { + 'API Quotient familial' => ['Quotient familial CAF & MSA', 'Identités allocataire et conjoint'] + } + }) + end + end + + context 'when the api is api_entreprise' do + before do + allow(scope_catalog).to receive(:lookup).with('unites_legales_etablissements_insee').and_return( + provider: 'INSEE', group: 'Informations générales', name: 'Données unités légales et établissements' + ) + end + + it 'nests scopes directly under provider, ignoring group' do + result = build_scopes(%w[unites_legales_etablissements_insee], 'api_entreprise') + + expect(result).to eq({ 'INSEE' => ['Données unités légales et établissements'] }) + end + end + + context 'when a scope is not found in the catalog' do + before do + allow(scope_catalog).to receive(:lookup).with('totally_unknown_scope').and_return(nil) + end - result = build_scopes(scopes, 'particulier') + it 'still shows the scope, under the fallback "Autres" group' do + result = build_scopes(['totally_unknown_scope'], 'api_entreprise') - expect(result).to eq({ - 'CNAF & MSA' => { - 'API Quotient familial' => ['QF'] - } - }) + expect(result).to eq({ 'Autres' => ['Totally unknown scope'] }) + end end - it 'doesnt fail when same provider has mixed scope label lengths' do - scopes = %w[pole_emploi_identite pole_emploi_paiements] + context 'when an api_particulier scope has a blank group but a sibling from the same provider has a real group' do + before do + allow(scope_catalog).to receive(:lookup).with('cnaf_allocataires').and_return( + provider: 'CNAF & MSA', group: 'API Quotient familial', name: 'Real Label' + ) + allow(scope_catalog).to receive(:lookup).with('cnaf_no_group').and_return( + provider: 'CNAF & MSA', group: nil, name: 'No Group Label' + ) + end - result = build_scopes(scopes, 'particulier') + it "keeps the nil-group scope's real name visible instead of losing it" do + result = build_scopes(%w[cnaf_allocataires cnaf_no_group], 'api_particulier') - expect(result).to eq({ - 'France Travail' => { - "API statut demandeur d'emploi" => ['Statut et identité du demandeur'], - 'API paiements France Travail' => [] - } - }) + expect(result).to eq({ + 'CNAF & MSA' => { + 'API Quotient familial' => ['Real Label'], + 'Autres' => ['No Group Label'] + } + }) + end end end end From adc499f1d0bd8c0ce252548858c6f0b15416746f Mon Sep 17 00:00:00 2001 From: Samuel-Zacharie FAURE Date: Tue, 11 Aug 2026 16:33:52 +0200 Subject: [PATCH 5/6] fix: pass full DataPass api id to build_scopes/humanize_scope --- .../api_particulier/endpoints/show.html.erb | 2 +- .../authorization_requests/show.html.erb | 2 +- .../authorization_request_show_spec.rb | 49 +++++++++++++++++++ .../api_particulier/endpoints/show_spec.rb | 24 +++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/site/app/views/api_particulier/endpoints/show.html.erb b/site/app/views/api_particulier/endpoints/show.html.erb index eb680f2826..ee32c20085 100644 --- a/site/app/views/api_particulier/endpoints/show.html.erb +++ b/site/app/views/api_particulier/endpoints/show.html.erb @@ -232,7 +232,7 @@
    <% @endpoint.scopes.each do |scope_name| %>
  • - <%= humanize_scope(scope_name, 'particulier').split('||').last.strip %> + <%= humanize_scope(scope_name, 'api_particulier').split('||').last.strip %> (<%= scope_name %>)
  • <% end %> diff --git a/site/app/views/shared/authorization_requests/show.html.erb b/site/app/views/shared/authorization_requests/show.html.erb index 9080a9b130..82622ab9e7 100644 --- a/site/app/views/shared/authorization_requests/show.html.erb +++ b/site/app/views/shared/authorization_requests/show.html.erb @@ -17,7 +17,7 @@
    - <% build_scopes(@authorization_request.scopes, namespace == 'api_entreprise' ? 'entreprise' : 'particulier').each_pair do |key, values| %> + <% build_scopes(@authorization_request.scopes, namespace).each_pair do |key, values| %>
    diff --git a/site/spec/features/api_particulier/authorization_request_show_spec.rb b/site/spec/features/api_particulier/authorization_request_show_spec.rb index 808136e006..66e108c3a8 100644 --- a/site/spec/features/api_particulier/authorization_request_show_spec.rb +++ b/site/spec/features/api_particulier/authorization_request_show_spec.rb @@ -135,6 +135,55 @@ expect(page).to have_text(distance_of_time_in_words(Time.zone.now, banned_token.blacklisted_at)) end + describe 'when DataPass provides scope definitions' do + let!(:scopes) { ['cnaf_quotient_familial'] } + + before do + Rails.cache.clear + + stub_request(:get, %r{#{Regexp.escape(DataPass::BASE_URL)}/api/v1/definitions/}) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { + 'scopes' => [ + { + 'value' => 'cnaf_quotient_familial', + 'name' => 'Quotient familial CAF & MSA', + 'group' => 'API Quotient familial', + 'provider' => 'CNAF & MSA' + } + ] + }.to_json + ) + + visit api_particulier_authorization_request_path(id: authorization_request.id) + end + + it 'displays the provider and label fetched from DataPass' do + expect(page).to have_text('CNAF & MSA') + expect(page).to have_text('Quotient familial CAF & MSA') + end + end + + describe 'when DataPass is unreachable' do + let!(:scopes) { ['cnaf_quotient_familial'] } + + before do + Rails.cache.clear + + stub_request(:get, %r{#{Regexp.escape(DataPass::BASE_URL)}/api/v1/definitions/}) + .to_timeout + + visit api_particulier_authorization_request_path(id: authorization_request.id) + end + + it 'still renders the page, falling back to a humanized version of the scope code' do + expect(page).to have_current_path(api_particulier_authorization_request_path(id: authorization_request.id), ignore_query: true) + expect(page).to have_text('Cnaf quotient familial') + end + end + describe 'when the user is demandeur' do describe 'when the token has less than 90 days left' do it 'displays the button to prolong the token' do diff --git a/site/spec/features/api_particulier/endpoints/show_spec.rb b/site/spec/features/api_particulier/endpoints/show_spec.rb index dd95a86a7c..ecce23d06d 100644 --- a/site/spec/features/api_particulier/endpoints/show_spec.rb +++ b/site/spec/features/api_particulier/endpoints/show_spec.rb @@ -53,5 +53,29 @@ expect(page).to have_css('h2#scopes + p + ul li code', text: '(cnaf_enfants)') expect(page).to have_css('h2#scopes + p + ul li code', text: '(cnaf_adresse)') end + + it 'renders the humanized scope label fetched from DataPass, not the raw scope code' do + Rails.cache.clear + + stub_request(:get, "#{DataPass::BASE_URL}/api/v1/definitions/api_particulier") + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { + 'scopes' => [ + { + 'value' => 'cnaf_quotient_familial', + 'name' => 'Quotient familial CAF & MSA', + 'group' => 'API Quotient familial', + 'provider' => 'CNAF & MSA' + } + ] + }.to_json + ) + + visit endpoint_path(uid:) + + expect(page).to have_css('h2#scopes + p + ul li strong', text: 'Quotient familial CAF & MSA') + end end end From bab38c6afc6d5de19ac23e5fb898e532ea8c9be0 Mon Sep 17 00:00:00 2001 From: Samuel-Zacharie FAURE Date: Wed, 12 Aug 2026 12:33:46 +0200 Subject: [PATCH 6/6] WIP --- .../authorization_request_mailer.rb | 5 ++-- .../authorization_request_mailer.rb | 5 ++-- .../_list_scopes.html.mjml | 10 +++----- .../_list_scopes.html.mjml | 10 +++----- site/config/i18n-tasks.yml | 1 - .../authorization_request_mailer_spec.rb | 2 +- .../mailers/authorization_request_mailer.rb | 25 ++++++++++++++++++- 7 files changed, 39 insertions(+), 19 deletions(-) diff --git a/site/app/mailers/api_entreprise/authorization_request_mailer.rb b/site/app/mailers/api_entreprise/authorization_request_mailer.rb index 1f2feef919..c30801711a 100644 --- a/site/app/mailers/api_entreprise/authorization_request_mailer.rb +++ b/site/app/mailers/api_entreprise/authorization_request_mailer.rb @@ -3,6 +3,8 @@ class APIEntreprise::AuthorizationRequestMailer < APIEntrepriseMailer include ExternalUrlHelper + helper :scope + %w[ embarquement_demande_refusee update_embarquement_demande_refusee @@ -20,9 +22,8 @@ class APIEntreprise::AuthorizationRequestMailer < APIEntrepriseMailer update_demande_recue ].each do |method| send('define_method', method) do |args| - @all_scopes = I18n.t('api_entreprise.tokens.token.scope') @authorization_request = args[:authorization_request] - @authorization_request_scopes = @authorization_request.scopes.map(&:to_sym).presence + @authorization_request_scopes = @authorization_request.scopes.presence @authorization_request_datapass_url = datapass_authorization_request_url(@authorization_request) @full_name_demandeur = @authorization_request.demandeur.full_name diff --git a/site/app/mailers/api_particulier/authorization_request_mailer.rb b/site/app/mailers/api_particulier/authorization_request_mailer.rb index ecca637505..bf7f8b1282 100644 --- a/site/app/mailers/api_particulier/authorization_request_mailer.rb +++ b/site/app/mailers/api_particulier/authorization_request_mailer.rb @@ -1,6 +1,8 @@ class APIParticulier::AuthorizationRequestMailer < APIParticulierMailer include ExternalUrlHelper + helper :scope + %w[ demande_recue update_demande_recue @@ -16,9 +18,8 @@ class APIParticulier::AuthorizationRequestMailer < APIParticulierMailer update_embarquement_valide_to_demandeur ].each do |method| send('define_method', method) do |args| - @all_scopes = I18n.t('api_particulier.tokens.token.scope') @authorization_request = args[:authorization_request] - @authorization_request_scopes = @authorization_request.scopes.map(&:to_sym).presence + @authorization_request_scopes = @authorization_request.scopes.presence @authorization_request_datapass_url = datapass_authorization_request_url(@authorization_request) @full_name_demandeur = @authorization_request.demandeur.full_name diff --git a/site/app/views/api_entreprise/authorization_request_mailer/_list_scopes.html.mjml b/site/app/views/api_entreprise/authorization_request_mailer/_list_scopes.html.mjml index f8360a9695..4f13d0a923 100644 --- a/site/app/views/api_entreprise/authorization_request_mailer/_list_scopes.html.mjml +++ b/site/app/views/api_entreprise/authorization_request_mailer/_list_scopes.html.mjml @@ -4,12 +4,10 @@ 🔐 Cette habilitation donne accès aux API suivantes :
      - <% @all_scopes.each do |scope_key, scope_data| %> - <% if (@authorization_request_scopes || []).include?(scope_key) %> -
    • - <%= scope_data[:label] %> -
    • - <% end %> + <% @authorization_request_scopes.each do |scope| %> +
    • + <%= humanize_scope(scope, 'api_entreprise') %> +
    • <% end %>
    diff --git a/site/app/views/api_particulier/authorization_request_mailer/_list_scopes.html.mjml b/site/app/views/api_particulier/authorization_request_mailer/_list_scopes.html.mjml index 2e3a3dc648..f43da9a576 100644 --- a/site/app/views/api_particulier/authorization_request_mailer/_list_scopes.html.mjml +++ b/site/app/views/api_particulier/authorization_request_mailer/_list_scopes.html.mjml @@ -3,12 +3,10 @@ 🔐 Cette habilitation donne accès aux API suivantes :
      - <% @all_scopes.each do |scope_key, scope_label| %> - <% if (@authorization_request_scopes || []).include?(scope_key) %> -
    • - <%= scope_label %> -
    • - <% end %> + <% (@authorization_request_scopes || []).each do |scope| %> +
    • + <%= humanize_scope(scope, 'api_particulier') %> +
    • <% end %>
    diff --git a/site/config/i18n-tasks.yml b/site/config/i18n-tasks.yml index f40e5f0cf6..f2c772f202 100644 --- a/site/config/i18n-tasks.yml +++ b/site/config/i18n-tasks.yml @@ -116,7 +116,6 @@ ignore_unused: - '*.sessions.after_logout.*' - 'api_{entreprise,particulier}.fiches_pratiques_entries.*' - 'api_{entreprise,particulier}.public_token_magic_links.show.*' - - 'api_{entreprise,particulier}.tokens.token.scope.*' - 'api_{entreprise,particulier}.documentation_pages.*.{title,sections}' - 'api_{entreprise,particulier}.token_mailer.*' - 'api_{entreprise,particulier}.authorization_request_mailer.*' diff --git a/site/spec/mailers/api_entreprise/authorization_request_mailer_spec.rb b/site/spec/mailers/api_entreprise/authorization_request_mailer_spec.rb index 2e8281a320..9b46963ec3 100644 --- a/site/spec/mailers/api_entreprise/authorization_request_mailer_spec.rb +++ b/site/spec/mailers/api_entreprise/authorization_request_mailer_spec.rb @@ -22,5 +22,5 @@ ], test_scopes: true, scope_test_method: 'embarquement_valide_to_demandeur_is_metier_not_tech', - scope_label: I18n.t('api_entreprise.tokens.token.scope.entreprises.label') + scope_label: 'INSEE || Data unités légales' end diff --git a/site/spec/support/shared_examples/mailers/authorization_request_mailer.rb b/site/spec/support/shared_examples/mailers/authorization_request_mailer.rb index f8b2bc2b4e..15692ff57e 100644 --- a/site/spec/support/shared_examples/mailers/authorization_request_mailer.rb +++ b/site/spec/support/shared_examples/mailers/authorization_request_mailer.rb @@ -38,7 +38,30 @@ end describe 'when there is a token' do - let(:authorization_request) { create(:authorization_request, :with_all_contacts, :with_tokens, scopes: ['entreprises']) } + let(:authorization_request) { create(:authorization_request, :with_all_contacts, :with_tokens, scopes: ['unites_legales_etablissements_insee']) } + + before do + stub_request(:post, "#{DataPass::BASE_URL}/api/oauth/token").to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { access_token: 'test_data_pass_access_token' }.to_json + ) + stub_request(:get, %r{#{Regexp.escape(DataPass::BASE_URL)}/api/v1/definitions/}) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { + 'scopes' => [ + { + 'value' => 'unites_legales_etablissements_insee', + 'name' => 'Data unités légales', + 'group' => 'Informations générales', + 'provider' => 'INSEE' + } + ] + }.to_json + ) + end it 'display scopes' do expect(subject.html_part.decoded).to include('Cette habilitation donne accès aux API suivantes')