diff --git a/.gitignore b/.gitignore
index 95de195166..c130e5d3ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,3 +67,5 @@ app/migration/.hubee_config.yml
lib/suivi_dtnum/sources/*
lib/suivi_dtnum/__pycache__
venv/
+
+/docs/shaping/*
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 1ae1b47143..c804384c9f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -10,8 +10,7 @@ Always use `make` commands - they handle Docker setup including Chrome for tests
- Run all tests: **`make tests`**
- Run specific test: `make tests spec/path/to/file_spec.rb:LINE_NUMBER`
- Run E2E tests: `make e2e` or `make e2e features/path/to/file.feature:LINE_NUMBER`
-- Run linter: `make lint`
-- Fix linting issues: `make fix-lint`
+- Run linter: `make fix-lint` (this fixes autocorrectable issues right away)
- JS linting: `make js-lint`
If a Docker command fails with missing dependencies (gems, packages, etc.), run `make build` to rebuild the image, then retry.
diff --git a/app/assets/stylesheets/components/definition_card.css b/app/assets/stylesheets/components/definition_card.css
new file mode 100644
index 0000000000..fdc60aa08a
--- /dev/null
+++ b/app/assets/stylesheets/components/definition_card.css
@@ -0,0 +1,16 @@
+.data-provider-card-img {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 4.2rem;
+ padding: .2em;
+}
+
+.data-provider-card-img img {
+ max-width: 4rem;
+ max-height: 4rem;
+}
+
+.definitions-search-count {
+ text-align: right;
+}
diff --git a/app/assets/stylesheets/components/wide_header.css b/app/assets/stylesheets/components/wide_header.css
new file mode 100644
index 0000000000..d56afa0a67
--- /dev/null
+++ b/app/assets/stylesheets/components/wide_header.css
@@ -0,0 +1,4 @@
+.wide-header-logo {
+ max-height: 5rem;
+ max-width: 10rem;
+}
diff --git a/app/assets/stylesheets/dsfr-extensions.css b/app/assets/stylesheets/dsfr-extensions.css
index 113711bb07..8feddc3033 100644
--- a/app/assets/stylesheets/dsfr-extensions.css
+++ b/app/assets/stylesheets/dsfr-extensions.css
@@ -308,3 +308,10 @@ fr-badge--grey {
.fr-table--align-middle th {
vertical-align: middle;
}
+
+code.code-inline {
+ background: var(--background-contrast-grey);
+ color: var(--text-default-grey);
+ padding: 0.125rem 0.5rem;
+ border-radius: 0.25rem;
+}
diff --git a/app/components/application_component.rb b/app/components/application_component.rb
index 9e1f4fe9df..5f3cdc91f9 100644
--- a/app/components/application_component.rb
+++ b/app/components/application_component.rb
@@ -1,5 +1,5 @@
class ApplicationComponent < ViewComponent::Base
include ApplicationHelper
- delegate :policy, to: :helpers
+ delegate :policy, :policy_scope, to: :helpers
end
diff --git a/app/components/instructor_menu_component.html.erb b/app/components/instructor_menu_component.html.erb
index e5aa3c95e0..2bd26e9184 100644
--- a/app/components/instructor_menu_component.html.erb
+++ b/app/components/instructor_menu_component.html.erb
@@ -4,6 +4,14 @@
+<% if show_definitions %>
+
">
<%= yield %>
diff --git a/app/views/layouts/header/_menu.html.erb b/app/views/layouts/header/_menu.html.erb
index 439f8e1d46..2e3bc2ba00 100644
--- a/app/views/layouts/header/_menu.html.erb
+++ b/app/views/layouts/header/_menu.html.erb
@@ -11,7 +11,8 @@
<%= render InstructorMenuComponent.new(
show_drafts: policy([:instruction, :instructor_draft_request]).enabled?,
show_templates: policy([:instruction, :message_template]).index?,
- show_user_rights: policy([:instruction, :user_right]).index?
+ show_user_rights: policy([:instruction, :user_right]).index?,
+ show_definitions: authorization_definitions_feature_enabled? && policy([:instruction, :authorization_definition]).index?
) %>
<% end %>
diff --git a/app/views/layouts/wide_container.html.erb b/app/views/layouts/wide_container.html.erb
new file mode 100644
index 0000000000..d05d93ae67
--- /dev/null
+++ b/app/views/layouts/wide_container.html.erb
@@ -0,0 +1,9 @@
+<%= content_for(:body) do %>
+
+ <%= render partial: 'shared/alerts' %>
+
+ <%= yield %>
+
+<% end %>
+
+<%= render template: 'layouts/application' %>
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index ff25a212ab..fe569ece0c 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -111,6 +111,7 @@ fr:
authorizations_and_requests: Demandes / habilitations
message_templates: Modèles de message
user_rights: Gestion des droits
+ formulaires: Formulaires
footer:
tagline: L'outil de gestion des habilitations juridiques pour les données à accès restreint.
external_links:
diff --git a/config/locales/instruction.fr.yml b/config/locales/instruction.fr.yml
index 32f6d5a809..e2d0ac8bd9 100644
--- a/config/locales/instruction.fr.yml
+++ b/config/locales/instruction.fr.yml
@@ -6,6 +6,20 @@ fr:
edit_templates_link: modifier les modèles
email_preview_accordion:
title: Voir un aperçu de l'email
+ authorization_definitions:
+ index:
+ title: Formulaires
+ empty: Aucun formulaire disponible.
+ subtitle: Tous les formulaires auxquels vous avez accès en lecture
+ search:
+ label: Rechercher un formulaire
+ placeholder: Nom du formulaire ou du fournisseur de données
+ submit: Rechercher
+ no_results: Aucun formulaire ne correspond à votre recherche.
+ results_count:
+ zero: Aucun formulaire
+ one: "%{count} formulaire"
+ other: "%{count} formulaires"
dashboard:
authorization_requests:
search:
diff --git a/config/locales/page_titles.fr.yml b/config/locales/page_titles.fr.yml
index 0220b3af14..d9271f3b1a 100644
--- a/config/locales/page_titles.fr.yml
+++ b/config/locales/page_titles.fr.yml
@@ -36,6 +36,7 @@ fr:
admin_user_organization_verifications: Vérification lien utilisateur / organisation
instruction_dashboard: Tableau de bord instructeur
+ instruction_definitions: Formulaires - Instruction
instruction_show: "Instruction %{definition_name} - %{authorization_request_name}"
instruction_initiated_requests: Demandes initiées par les instructeurs
instruction_draft_requests_new: Initier une demande d’habilitation
diff --git a/config/routes.rb b/config/routes.rb
index 57fb3a4f5a..d25d5ec79c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -117,6 +117,8 @@
namespace :instruction do
get '/tableau-de-bord/:id', to: 'dashboard#show', as: :dashboard_show
+ resources :authorization_definitions, only: [:index], path: 'formulaires'
+
resources :message_templates, only: %i[index new create edit update destroy], path: 'modeles-messages'
resources :authorization_requests, only: %w[show], path: 'demandes' do
diff --git a/features/instructeurs/gestion_des_formulaires/liste_formulaires.feature b/features/instructeurs/gestion_des_formulaires/liste_formulaires.feature
new file mode 100644
index 0000000000..77cecf7099
--- /dev/null
+++ b/features/instructeurs/gestion_des_formulaires/liste_formulaires.feature
@@ -0,0 +1,33 @@
+# language: fr
+
+Fonctionnalité: Liste des formulaires pour les instructeurs
+ En tant qu'instructeur, je peux consulter la liste des formulaires
+ auxquels j'ai accès, afin de gérer mes habilitations.
+
+ Contexte:
+ Soit un fournisseur de données "DINUM" existe
+ Sachant que je suis un rapporteur "API Entreprise"
+ Et que je me connecte
+
+ Scénario: Je vois le lien vers la liste des formulaires dans l'espace instruction
+ Quand je me rends sur mon tableau de bord instructeur
+ Alors il y a un bouton "Formulaires"
+
+ Scénario: Je peux accéder à la liste des formulaires
+ Quand je me rends sur la liste des formulaires
+ Alors la page contient "API Entreprise"
+
+ Scénario: Je ne vois que les formulaires pour lesquels j'ai un rôle
+ Quand je me rends sur la liste des formulaires
+ Alors la page contient "API Entreprise"
+ Et la page ne contient pas "API Particulier"
+
+ Scénario: Les compteurs de demandes affichés sur un formulaire sont corrects
+ Sachant qu'il y a 2 demandes d'habilitation "API Entreprise" validées
+ Et qu'il y a 1 demande d'habilitation "API Entreprise" en attente
+ Quand je me rends sur la liste des formulaires
+ Alors le formulaire "API Entreprise" affiche 2 demandes validées et 1 demande en cours
+
+ Scénario: La description affiche le fournisseur de données
+ Quand je me rends sur la liste des formulaires
+ Alors la page contient "DINUM"
diff --git a/features/step_definitions/instructions_steps.rb b/features/step_definitions/instructions_steps.rb
index bd3c54a6a1..e709a1abb9 100644
--- a/features/step_definitions/instructions_steps.rb
+++ b/features/step_definitions/instructions_steps.rb
@@ -1,3 +1,16 @@
Alors("je suis sur l'espace instruction") do
expect(page).to have_current_path(/instruction/)
end
+
+Quand('je me rends sur la liste des formulaires') do
+ visit instruction_authorization_definitions_path
+end
+
+Alors('le formulaire {string} affiche {int} demande(s) validée(s) et {int} demande(s) en cours') do |name, validated_count, submitted_count|
+ definition = find_authorization_definition_from_name(name)
+
+ within(css_id(definition)) do
+ counts = all('.fr-card__footer span:not(.fr-badge)').map(&:text)
+ expect(counts).to eq([validated_count.to_s, submitted_count.to_s])
+ end
+end
diff --git a/spec/components/instructor_menu_component_spec.rb b/spec/components/instructor_menu_component_spec.rb
index a6fbaa1925..a76ba8e853 100644
--- a/spec/components/instructor_menu_component_spec.rb
+++ b/spec/components/instructor_menu_component_spec.rb
@@ -1,4 +1,6 @@
RSpec.describe InstructorMenuComponent, type: :component do
+ include Rails.application.routes.url_helpers
+
describe '#render?' do
context 'when all flags are false' do
it 'does not render' do
@@ -87,5 +89,16 @@
expect(page).to have_no_link(I18n.t('layouts.header.menu.instruction.user_rights'))
end
end
+
+ context 'when show_definitions is true' do
+ it 'links to the definitions list' do
+ component = described_class.new(show_drafts: false, show_templates: false, show_user_rights: false, show_definitions: true)
+
+ render_inline(component)
+
+ expect(page).to have_link(I18n.t('layouts.header.menu.instruction.formulaires'),
+ href: instruction_authorization_definitions_path)
+ end
+ end
end
end
diff --git a/spec/components/previews/molecules/instruction/authorization_definition/card_component_preview.rb b/spec/components/previews/molecules/instruction/authorization_definition/card_component_preview.rb
new file mode 100644
index 0000000000..b0d7035b76
--- /dev/null
+++ b/spec/components/previews/molecules/instruction/authorization_definition/card_component_preview.rb
@@ -0,0 +1,10 @@
+class Molecules::Instruction::AuthorizationDefinition::CardComponentPreview < ApplicationPreview
+ def default
+ definition = AuthorizationDefinition.find('api_entreprise')
+ render Molecules::Instruction::AuthorizationDefinition::CardComponent.new(
+ authorization_definition: definition,
+ validated_count: 1373,
+ submitted_count: 6
+ )
+ end
+end
diff --git a/spec/components/previews/molecules/instruction/wide_header_preview.rb b/spec/components/previews/molecules/instruction/wide_header_preview.rb
new file mode 100644
index 0000000000..7a17564a29
--- /dev/null
+++ b/spec/components/previews/molecules/instruction/wide_header_preview.rb
@@ -0,0 +1,31 @@
+class Molecules::Instruction::WideHeaderPreview < ApplicationPreview
+ def minimal
+ render Molecules::Instruction::WideHeader.new(title: 'Fournisseurs de données')
+ end
+
+ def data_providers_index
+ render Molecules::Instruction::WideHeader.new(
+ title: 'Fournisseurs de données',
+ dsfr_logo: 'artwork/pictograms/buildings/city-hall.svg'
+ ) do |component|
+ component.with_subtitle_content do
+ tag.p('Choisissez un fournisseur de données pour gérer ses formulaires', class: 'fr-mb-0')
+ end
+ end
+ end
+
+ # @label Data Provider's Definitions
+ def data_provider_definitions
+ data_provider = DataProvider.first!
+ render Molecules::Instruction::WideHeader.new(
+ logo_asset: data_provider.logo,
+ title: data_provider.name,
+ back_link: { path: '#', text: 'Fournisseurs de données' }
+ ) do |component|
+ component.with_subtitle_content do
+ ActionController::Base.helpers.link_to data_provider.link, data_provider.link,
+ target: '_blank', rel: 'noopener external', class: 'fr-link'
+ end
+ end
+ end
+end
diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb
index de1b09df28..fd8ce098f5 100644
--- a/spec/i18n_spec.rb
+++ b/spec/i18n_spec.rb
@@ -15,7 +15,7 @@
it 'does not have unused keys' do
expect(unused_keys).to be_empty,
- "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them"
+ "#{unused_keys.leaves.count} unused i18n keys:\n#{unused_keys.leaves.map { |leaf| " #{leaf.full_key(root: false)}" }.join("\n")}"
end
it 'does not have inconsistent interpolations' do
diff --git a/spec/models/concerns/user_roles_spec.rb b/spec/models/concerns/user_roles_spec.rb
new file mode 100644
index 0000000000..b1cf88a465
--- /dev/null
+++ b/spec/models/concerns/user_roles_spec.rb
@@ -0,0 +1,402 @@
+RSpec.describe UserRoles do
+ describe '#managed_fd_slugs' do
+ subject { user.managed_fd_slugs }
+
+ context 'when user has no FD-level manager role' do
+ let(:user) { build(:user, :manager, authorization_request_types: %w[api_entreprise]) }
+
+ it { is_expected.to eq([]) }
+ end
+
+ context 'when user has FD-level manager roles' do
+ let(:user) { build(:user, roles: %w[dinum:*:manager dgfip:*:manager dinum:api_entreprise:instructor]) }
+
+ it { is_expected.to contain_exactly('dinum', 'dgfip') }
+ end
+
+ context 'when user has FD-level non-manager role' do
+ let(:user) { build(:user, roles: %w[dinum:*:reporter]) }
+
+ it { is_expected.to eq([]) }
+ end
+ end
+
+ describe '#manages_role?' do
+ subject(:result) { manager.manages_role?(role) }
+
+ context 'when manager has the manager role on the definition' do
+ let(:manager) { create(:user, :manager, authorization_request_types: %i[api_entreprise]) }
+
+ context 'when the role is on the same definition' do
+ let(:role) { 'dinum:api_entreprise:reporter' }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when the role is on another definition' do
+ let(:role) { 'dinum:api_particulier:reporter' }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when the role is FD-wildcard for the same provider' do
+ let(:role) { 'dinum:*:reporter' }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ context 'when manager has the FD-wildcard manager role' do
+ let(:manager) { create(:user, roles: ['dinum:*:manager']) }
+
+ context 'when the role is FD-wildcard on the same provider' do
+ let(:role) { 'dinum:*:reporter' }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when the role is on a definition of the same provider' do
+ let(:role) { 'dinum:api_entreprise:reporter' }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when the role is on a definition of another provider' do
+ let(:role) { 'dgfip:api_impot_particulier:reporter' }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ context 'when the role is admin' do
+ let(:manager) { create(:user, roles: ['dinum:*:manager']) }
+ let(:role) { 'admin' }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when the role string is malformed' do
+ let(:manager) { create(:user, :manager, authorization_request_types: %i[api_entreprise]) }
+ let(:role) { 'not-a-valid-role' }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '#managed_by?' do
+ subject(:result) { target.managed_by?(manager) }
+
+ let(:manager) { create(:user, :manager, authorization_request_types: %i[api_entreprise]) }
+
+ context 'when the target has at least one role within the manager scope' do
+ let(:target) { create(:user, roles: %w[dinum:api_entreprise:reporter dinum:api_particulier:instructor]) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when the target has no role within the manager scope' do
+ let(:target) { create(:user, roles: %w[dinum:api_particulier:instructor]) }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when the target has no roles at all' do
+ let(:target) { create(:user, roles: []) }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '#reporter?' do
+ subject { user.reporter?(authorization_request_type) }
+
+ context 'when user is an admin' do
+ let(:user) { create(:user, :admin) }
+
+ context 'without authorization_request_type' do
+ let(:authorization_request_type) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with authorization_request_type' do
+ let(:authorization_request_type) { 'api_entreprise' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when user is not a reporter' do
+ let(:user) { build(:user) }
+
+ context 'without authorization_request_type' do
+ let(:authorization_request_type) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with authorization_request_type' do
+ let(:authorization_request_type) { 'api_entreprise' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when user is a reporter' do
+ let(:user) { build(:user, :reporter, authorization_request_types: %w[api_entreprise]) }
+
+ context 'without authorization_request_type' do
+ let(:authorization_request_type) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with authorization_request_type' do
+ context 'when authorization_request_type matches' do
+ let(:authorization_request_type) { 'api_entreprise' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when authorization_request_type does not matche' do
+ let(:authorization_request_type) { 'api_particulier' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ context 'when user is an instructor' do
+ let(:user) { build(:user, :instructor, authorization_request_types: %w[api_entreprise]) }
+
+ context 'without authorization_request_type' do
+ let(:authorization_request_type) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with authorization_request_type' do
+ context 'when authorization_request_type matches' do
+ let(:authorization_request_type) { 'api_entreprise' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when authorization_request_type does not matche' do
+ let(:authorization_request_type) { 'api_particulier' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ context 'when user is a manager' do
+ let(:user) { build(:user, :manager, authorization_request_types: %w[api_entreprise]) }
+
+ context 'without authorization_request_type' do
+ let(:authorization_request_type) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with authorization_request_type' do
+ context 'when authorization_request_type matches' do
+ let(:authorization_request_type) { 'api_entreprise' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when authorization_request_type does not matche' do
+ let(:authorization_request_type) { 'api_particulier' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+ end
+
+ describe '#fd_reporter?' do
+ context 'when user has an FD-level reporter role on the provider' do
+ let(:user) { create(:user, :fd_reporter, data_provider_slugs: ['dinum']) }
+
+ it { expect(user.fd_reporter?('dinum')).to be true }
+ end
+
+ context 'when user has a definition-level reporter role under the provider' do
+ let(:user) { create(:user, :reporter, authorization_request_types: %w[api_entreprise]) }
+
+ it { expect(user.fd_reporter?('dinum')).to be true }
+ end
+
+ context 'when user has a role on another provider' do
+ let(:user) { create(:user, :fd_reporter, data_provider_slugs: ['dgfip']) }
+
+ it { expect(user.fd_reporter?('dinum')).to be false }
+ end
+
+ context 'when user is an admin' do
+ let(:user) { create(:user, :admin) }
+
+ it { expect(user.fd_reporter?('dinum')).to be true }
+ end
+
+ context 'when user has a manager role on the provider' do
+ let(:user) { create(:user, :fd_manager, data_provider_slugs: ['dinum']) }
+
+ it { expect(user.fd_reporter?('dinum')).to be true }
+ end
+
+ context 'when user has no role' do
+ let(:user) { create(:user) }
+
+ it { expect(user.fd_reporter?('dinum')).to be false }
+ end
+ end
+
+ describe '#instructor?' do
+ subject { user.instructor?(authorization_request_type) }
+
+ context 'when user is not an instructor' do
+ let(:user) { build(:user) }
+
+ context 'without authorization_request_type' do
+ let(:authorization_request_type) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with authorization_request_type' do
+ let(:authorization_request_type) { 'api_entreprise' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when user is an instructor' do
+ let(:user) { build(:user, :instructor, authorization_request_types: %w[api_entreprise]) }
+
+ context 'without authorization_request_type' do
+ let(:authorization_request_type) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with authorization_request_type' do
+ context 'when authorization_request_type matches' do
+ let(:authorization_request_type) { 'api_entreprise' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when authorization_request_type does not matche' do
+ let(:authorization_request_type) { 'api_particulier' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ context 'when user is a manager' do
+ let(:user) { build(:user, :manager, authorization_request_types: %w[api_entreprise]) }
+
+ context 'without authorization_request_type' do
+ let(:authorization_request_type) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with authorization_request_type' do
+ context 'when authorization_request_type matches' do
+ let(:authorization_request_type) { 'api_entreprise' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when authorization_request_type does not matche' do
+ let(:authorization_request_type) { 'api_particulier' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+ end
+
+ describe '#manager?' do
+ subject { user.manager?(authorization_request_type) }
+
+ context 'when user is not a manager' do
+ let(:user) { build(:user) }
+
+ context 'without authorization_request_type' do
+ let(:authorization_request_type) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with authorization_request_type' do
+ let(:authorization_request_type) { 'api_entreprise' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when user is a manager' do
+ let(:user) { build(:user, :manager, authorization_request_types: %w[api_entreprise]) }
+
+ context 'without authorization_request_type' do
+ let(:authorization_request_type) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with authorization_request_type' do
+ context 'when authorization_request_type matches' do
+ let(:authorization_request_type) { 'api_entreprise' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when authorization_request_type does not matche' do
+ let(:authorization_request_type) { 'api_particulier' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+ end
+
+ describe '#authorization_definition_roles_as' do
+ subject { user.authorization_definition_roles_as(kind).map(&:id) }
+
+ let(:kind) { 'instructor' }
+
+ context 'when user is not an instructor' do
+ let(:user) { build(:user) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when user is an instructor' do
+ let(:user) { build(:user, :instructor, authorization_request_types:) }
+ let(:authorization_request_types) { %w[api_entreprise api_particulier] }
+
+ let(:api_entreprise_definition) { AuthorizationDefinition.find('api_entreprise') }
+ let(:api_particulier_definition) { AuthorizationDefinition.find('api_particulier') }
+
+ it { is_expected.to contain_exactly('api_entreprise', 'api_particulier') }
+ end
+
+ context 'when the user is reporter and developer for the same authorization definition' do
+ let(:kind) { 'reporter' }
+
+ let(:user) { build(:user, :instructor, authorization_request_types: %w[api_entreprise]) }
+
+ before do
+ user.grant_role(:developer, 'api_entreprise')
+ user.save
+ end
+
+ it { is_expected.to contain_exactly('api_entreprise') }
+ end
+ end
+end
diff --git a/spec/models/role_set_spec.rb b/spec/models/role_set_spec.rb
index 9b899136ad..b1f374aac1 100644
--- a/spec/models/role_set_spec.rb
+++ b/spec/models/role_set_spec.rb
@@ -97,6 +97,29 @@
end
end
+ describe '#provider_slugs' do
+ it 'returns unique provider slugs for matching roles' do
+ role_set = described_class.new(
+ %w[dinum:api_entreprise:reporter dinum:api_particulier:reporter dgfip:api_impot_particulier_fc_sandbox:reporter],
+ :reporter,
+ )
+
+ expect(role_set.provider_slugs).to match_array(%w[dinum dgfip])
+ end
+
+ it 'includes FD-level wildcard provider slugs' do
+ role_set = described_class.new(%w[dinum:*:reporter], :reporter)
+
+ expect(role_set.provider_slugs).to eq(%w[dinum])
+ end
+
+ it 'returns empty array when no matching roles' do
+ role_set = described_class.new(%w[], :reporter)
+
+ expect(role_set.provider_slugs).to be_empty
+ end
+ end
+
describe '#authorization_request_types' do
it 'returns classified authorization request types' do
role_set = described_class.new(%w[dinum:api_entreprise:instructor], :instructor)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3cd67071d6..2f1cb0c30f 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -115,369 +115,6 @@
end
end
- describe '#managed_fd_slugs' do
- subject { user.managed_fd_slugs }
-
- context 'when user has no FD-level manager role' do
- let(:user) { build(:user, :manager, authorization_request_types: %w[api_entreprise]) }
-
- it { is_expected.to eq([]) }
- end
-
- context 'when user has FD-level manager roles' do
- let(:user) { build(:user, roles: %w[dinum:*:manager dgfip:*:manager dinum:api_entreprise:instructor]) }
-
- it { is_expected.to contain_exactly('dinum', 'dgfip') }
- end
-
- context 'when user has FD-level non-manager role' do
- let(:user) { build(:user, roles: %w[dinum:*:reporter]) }
-
- it { is_expected.to eq([]) }
- end
- end
-
- describe '#manages_role?' do
- subject(:result) { manager.manages_role?(role) }
-
- context 'when manager has the manager role on the definition' do
- let(:manager) { create(:user, :manager, authorization_request_types: %i[api_entreprise]) }
-
- context 'when the role is on the same definition' do
- let(:role) { 'dinum:api_entreprise:reporter' }
-
- it { is_expected.to be true }
- end
-
- context 'when the role is on another definition' do
- let(:role) { 'dinum:api_particulier:reporter' }
-
- it { is_expected.to be false }
- end
-
- context 'when the role is FD-wildcard for the same provider' do
- let(:role) { 'dinum:*:reporter' }
-
- it { is_expected.to be false }
- end
- end
-
- context 'when manager has the FD-wildcard manager role' do
- let(:manager) { create(:user, roles: ['dinum:*:manager']) }
-
- context 'when the role is FD-wildcard on the same provider' do
- let(:role) { 'dinum:*:reporter' }
-
- it { is_expected.to be true }
- end
-
- context 'when the role is on a definition of the same provider' do
- let(:role) { 'dinum:api_entreprise:reporter' }
-
- it { is_expected.to be true }
- end
-
- context 'when the role is on a definition of another provider' do
- let(:role) { 'dgfip:api_impot_particulier:reporter' }
-
- it { is_expected.to be false }
- end
- end
-
- context 'when the role is admin' do
- let(:manager) { create(:user, roles: ['dinum:*:manager']) }
- let(:role) { 'admin' }
-
- it { is_expected.to be false }
- end
-
- context 'when the role string is malformed' do
- let(:manager) { create(:user, :manager, authorization_request_types: %i[api_entreprise]) }
- let(:role) { 'not-a-valid-role' }
-
- it { is_expected.to be false }
- end
- end
-
- describe '#managed_by?' do
- subject(:result) { target.managed_by?(manager) }
-
- let(:manager) { create(:user, :manager, authorization_request_types: %i[api_entreprise]) }
-
- context 'when the target has at least one role within the manager scope' do
- let(:target) { create(:user, roles: %w[dinum:api_entreprise:reporter dinum:api_particulier:instructor]) }
-
- it { is_expected.to be true }
- end
-
- context 'when the target has no role within the manager scope' do
- let(:target) { create(:user, roles: %w[dinum:api_particulier:instructor]) }
-
- it { is_expected.to be false }
- end
-
- context 'when the target has no roles at all' do
- let(:target) { create(:user, roles: []) }
-
- it { is_expected.to be false }
- end
- end
-
- describe '#reporter?' do
- subject { user.reporter?(authorization_request_type) }
-
- context 'when user is an admin' do
- let(:user) { create(:user, :admin) }
-
- context 'without authorization_request_type' do
- let(:authorization_request_type) { nil }
-
- it { is_expected.to be_truthy }
- end
-
- context 'with authorization_request_type' do
- let(:authorization_request_type) { 'api_entreprise' }
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'when user is not a reporter' do
- let(:user) { build(:user) }
-
- context 'without authorization_request_type' do
- let(:authorization_request_type) { nil }
-
- it { is_expected.to be_falsey }
- end
-
- context 'with authorization_request_type' do
- let(:authorization_request_type) { 'api_entreprise' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when user is a reporter' do
- let(:user) { build(:user, :reporter, authorization_request_types: %w[api_entreprise]) }
-
- context 'without authorization_request_type' do
- let(:authorization_request_type) { nil }
-
- it { is_expected.to be_truthy }
- end
-
- context 'with authorization_request_type' do
- context 'when authorization_request_type matches' do
- let(:authorization_request_type) { 'api_entreprise' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when authorization_request_type does not matche' do
- let(:authorization_request_type) { 'api_particulier' }
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
- context 'when user is an instructor' do
- let(:user) { build(:user, :instructor, authorization_request_types: %w[api_entreprise]) }
-
- context 'without authorization_request_type' do
- let(:authorization_request_type) { nil }
-
- it { is_expected.to be_truthy }
- end
-
- context 'with authorization_request_type' do
- context 'when authorization_request_type matches' do
- let(:authorization_request_type) { 'api_entreprise' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when authorization_request_type does not matche' do
- let(:authorization_request_type) { 'api_particulier' }
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
- context 'when user is a manager' do
- let(:user) { build(:user, :manager, authorization_request_types: %w[api_entreprise]) }
-
- context 'without authorization_request_type' do
- let(:authorization_request_type) { nil }
-
- it { is_expected.to be_truthy }
- end
-
- context 'with authorization_request_type' do
- context 'when authorization_request_type matches' do
- let(:authorization_request_type) { 'api_entreprise' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when authorization_request_type does not matche' do
- let(:authorization_request_type) { 'api_particulier' }
-
- it { is_expected.to be_falsey }
- end
- end
- end
- end
-
- describe '#instructor?' do
- subject { user.instructor?(authorization_request_type) }
-
- context 'when user is not an instructor' do
- let(:user) { build(:user) }
-
- context 'without authorization_request_type' do
- let(:authorization_request_type) { nil }
-
- it { is_expected.to be_falsey }
- end
-
- context 'with authorization_request_type' do
- let(:authorization_request_type) { 'api_entreprise' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when user is an instructor' do
- let(:user) { build(:user, :instructor, authorization_request_types: %w[api_entreprise]) }
-
- context 'without authorization_request_type' do
- let(:authorization_request_type) { nil }
-
- it { is_expected.to be_truthy }
- end
-
- context 'with authorization_request_type' do
- context 'when authorization_request_type matches' do
- let(:authorization_request_type) { 'api_entreprise' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when authorization_request_type does not matche' do
- let(:authorization_request_type) { 'api_particulier' }
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
- context 'when user is a manager' do
- let(:user) { build(:user, :manager, authorization_request_types: %w[api_entreprise]) }
-
- context 'without authorization_request_type' do
- let(:authorization_request_type) { nil }
-
- it { is_expected.to be_truthy }
- end
-
- context 'with authorization_request_type' do
- context 'when authorization_request_type matches' do
- let(:authorization_request_type) { 'api_entreprise' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when authorization_request_type does not matche' do
- let(:authorization_request_type) { 'api_particulier' }
-
- it { is_expected.to be_falsey }
- end
- end
- end
- end
-
- describe '#manager?' do
- subject { user.manager?(authorization_request_type) }
-
- context 'when user is not a manager' do
- let(:user) { build(:user) }
-
- context 'without authorization_request_type' do
- let(:authorization_request_type) { nil }
-
- it { is_expected.to be_falsey }
- end
-
- context 'with authorization_request_type' do
- let(:authorization_request_type) { 'api_entreprise' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when user is a manager' do
- let(:user) { build(:user, :manager, authorization_request_types: %w[api_entreprise]) }
-
- context 'without authorization_request_type' do
- let(:authorization_request_type) { nil }
-
- it { is_expected.to be_truthy }
- end
-
- context 'with authorization_request_type' do
- context 'when authorization_request_type matches' do
- let(:authorization_request_type) { 'api_entreprise' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when authorization_request_type does not matche' do
- let(:authorization_request_type) { 'api_particulier' }
-
- it { is_expected.to be_falsey }
- end
- end
- end
- end
-
- describe '#authorization_definition_roles_as' do
- subject { user.authorization_definition_roles_as(kind).map(&:id) }
-
- let(:kind) { 'instructor' }
-
- context 'when user is not an instructor' do
- let(:user) { build(:user) }
-
- it { is_expected.to be_empty }
- end
-
- context 'when user is an instructor' do
- let(:user) { build(:user, :instructor, authorization_request_types:) }
- let(:authorization_request_types) { %w[api_entreprise api_particulier] }
-
- let(:api_entreprise_definition) { AuthorizationDefinition.find('api_entreprise') }
- let(:api_particulier_definition) { AuthorizationDefinition.find('api_particulier') }
-
- it { is_expected.to contain_exactly('api_entreprise', 'api_particulier') }
- end
-
- context 'when the user is reporter and developer for the same authorization definition' do
- let(:kind) { 'reporter' }
-
- let(:user) { build(:user, :instructor, authorization_request_types: %w[api_entreprise]) }
-
- before do
- user.grant_role(:developer, 'api_entreprise')
- user.save
- end
-
- it { is_expected.to contain_exactly('api_entreprise') }
- end
- end
-
describe '#settings on instruction_submit_notifications' do
subject { user.instruction_submit_notifications_for_api_entreprise }
diff --git a/spec/policies/instruction/authorization_definition_policy_spec.rb b/spec/policies/instruction/authorization_definition_policy_spec.rb
new file mode 100644
index 0000000000..85a5abe374
--- /dev/null
+++ b/spec/policies/instruction/authorization_definition_policy_spec.rb
@@ -0,0 +1,37 @@
+RSpec.describe Instruction::AuthorizationDefinitionPolicy do
+ subject(:policy) { described_class.new(UserContext.new(user), AuthorizationDefinition) }
+
+ describe '#index?' do
+ subject { policy.index? }
+
+ context 'when user is an admin' do
+ let(:user) { create(:user, :admin) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when user is a reporter' do
+ let(:user) { create(:user, :reporter, authorization_request_types: %i[api_entreprise]) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when user is a manager' do
+ let(:user) { create(:user, :manager, authorization_request_types: %i[api_entreprise]) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when user is an instructor' do
+ let(:user) { create(:user, :instructor, authorization_request_types: %i[api_entreprise]) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when user has no role' do
+ let(:user) { create(:user) }
+
+ it { is_expected.to be false }
+ end
+ end
+end