Merge branch 'main' into 6649-etq-usager-instructeur-rendre-la-suppression-plus-visible
This commit is contained in:
commit
e7d9d047fe
59 changed files with 686 additions and 240 deletions
19
Gemfile.lock
19
Gemfile.lock
|
@ -67,8 +67,11 @@ GEM
|
|||
activemodel (>= 4.1, < 6.2)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
active_storage_validations (0.9.2)
|
||||
rails (>= 5.2.0)
|
||||
active_storage_validations (0.9.6)
|
||||
activejob (>= 5.2.0)
|
||||
activemodel (>= 5.2.0)
|
||||
activestorage (>= 5.2.0)
|
||||
activesupport (>= 5.2.0)
|
||||
activejob (6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
globalid (>= 0.3.6)
|
||||
|
@ -376,7 +379,7 @@ GEM
|
|||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.2.2)
|
||||
jwt (2.3.0)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
|
@ -421,7 +424,7 @@ GEM
|
|||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.0212)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.1)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.6.1)
|
||||
minitest (5.14.4)
|
||||
momentjs-rails (2.20.1)
|
||||
|
@ -437,7 +440,7 @@ GEM
|
|||
mini_portile2 (~> 2.6.1)
|
||||
racc (~> 1.4)
|
||||
open4 (1.3.4)
|
||||
openid_connect (1.2.0)
|
||||
openid_connect (1.3.0)
|
||||
activemodel
|
||||
attr_required (>= 1.0.0)
|
||||
json-jwt (>= 1.5.0)
|
||||
|
@ -492,7 +495,7 @@ GEM
|
|||
rack (>= 1.0, < 3)
|
||||
rack-mini-profiler (2.3.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-oauth2 (1.16.0)
|
||||
rack-oauth2 (1.19.0)
|
||||
activesupport
|
||||
attr_required
|
||||
httpclient
|
||||
|
@ -700,7 +703,7 @@ GEM
|
|||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
swd (1.2.0)
|
||||
swd (1.3.0)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
httpclient (>= 2.4)
|
||||
|
@ -747,7 +750,7 @@ GEM
|
|||
nokogiri (~> 1.6)
|
||||
rubyzip (>= 1.3.0)
|
||||
selenium-webdriver (>= 3.0, < 4.0)
|
||||
webfinger (1.1.0)
|
||||
webfinger (1.2.0)
|
||||
activesupport
|
||||
httpclient (>= 2.4)
|
||||
webmock (3.11.2)
|
||||
|
|
BIN
app/assets/images/logo-agent-connect.png
Normal file
BIN
app/assets/images/logo-agent-connect.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
11
app/assets/stylesheets/france-connect-agent-login.scss
Normal file
11
app/assets/stylesheets/france-connect-agent-login.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
|
||||
.france-connect-agent-login-button {
|
||||
background-image: image-url("logo-agent-connect.png");
|
||||
display: block;
|
||||
height: 60px;
|
||||
width: 230px;
|
||||
margin: 20px auto;
|
||||
font-size: 0;
|
||||
}
|
53
app/controllers/agent_connect/agent_controller.rb
Normal file
53
app/controllers/agent_connect/agent_controller.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
# doc: https://github.com/france-connect/Documentation-AgentConnect
|
||||
class AgentConnect::AgentController < ApplicationController
|
||||
before_action :redirect_to_login_if_fc_aborted, only: [:callback]
|
||||
|
||||
def index
|
||||
end
|
||||
|
||||
def login
|
||||
redirect_to AgentConnectService.authorization_uri
|
||||
end
|
||||
|
||||
def callback
|
||||
user_info = AgentConnectService.user_info(params[:code])
|
||||
|
||||
instructeur = Instructeur.find_by(agent_connect_id: user_info['sub'])
|
||||
|
||||
if instructeur.nil?
|
||||
instructeur = Instructeur.find_by(users: { email: santized_email(user_info) })
|
||||
instructeur&.update(agent_connect_id: user_info['sub'])
|
||||
end
|
||||
|
||||
if instructeur.nil?
|
||||
user = User.create_or_promote_to_instructeur(santized_email(user_info), Devise.friendly_token[0, 20])
|
||||
instructeur = user.instructeur
|
||||
instructeur.update(agent_connect_id: user_info['sub'])
|
||||
end
|
||||
|
||||
sign_in(:user, instructeur.user)
|
||||
|
||||
redirect_to instructeur_procedures_path
|
||||
|
||||
rescue Rack::OAuth2::Client::Error => e
|
||||
Rails.logger.error e.message
|
||||
redirect_france_connect_error_connection
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def santized_email(user_info)
|
||||
user_info['email'].strip.downcase
|
||||
end
|
||||
|
||||
def redirect_to_login_if_fc_aborted
|
||||
if params[:code].blank?
|
||||
redirect_to new_user_session_path
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_france_connect_error_connection
|
||||
flash.alert = t('errors.messages.france_connect.connexion')
|
||||
redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
|
@ -1,4 +1,6 @@
|
|||
class API::V2::GraphqlController < API::V2::BaseController
|
||||
include GraphqlOperationLogConcern
|
||||
|
||||
def execute
|
||||
variables = ensure_hash(params[:variables])
|
||||
|
||||
|
@ -24,43 +26,10 @@ class API::V2::GraphqlController < API::V2::BaseController
|
|||
super
|
||||
|
||||
payload.merge!({
|
||||
graphql_operation: operation_log(params[:query], params[:operationName], params[:variables])
|
||||
graphql_operation: operation_log(params[:query], params[:operationName], params[:variables]&.to_unsafe_h)
|
||||
})
|
||||
end
|
||||
|
||||
def operation_log(query, operation_name, variables)
|
||||
return "NoQuery" if query.nil?
|
||||
|
||||
operation = GraphQL.parse(query).children.find do |node|
|
||||
if node.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
||||
node.name == operation_name
|
||||
end
|
||||
end
|
||||
|
||||
return "InvalidQuery" if operation.nil?
|
||||
return "IntrospectionQuery" if operation.name == "IntrospectionQuery"
|
||||
|
||||
message = operation.operation_type
|
||||
if operation.name
|
||||
message += ": #{operation.name} { "
|
||||
end
|
||||
message += operation.selections.map(&:name).join(', ')
|
||||
message += " }"
|
||||
if variables.present?
|
||||
message += " "
|
||||
message += variables.to_unsafe_h.flat_map do |(name, value)|
|
||||
if name == "input"
|
||||
value.map do |(name, value)|
|
||||
"#{name}: \"#{value.to_s.truncate(10)}\""
|
||||
end
|
||||
else
|
||||
"#{name}: \"#{value.to_s.truncate(10)}\""
|
||||
end
|
||||
end.join(', ')
|
||||
end
|
||||
message
|
||||
end
|
||||
|
||||
def process_action(*args)
|
||||
super
|
||||
rescue ActionDispatch::Http::Parameters::ParseError => exception
|
||||
|
|
62
app/controllers/concerns/graphql_operation_log_concern.rb
Normal file
62
app/controllers/concerns/graphql_operation_log_concern.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
module GraphqlOperationLogConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# This method parses GraphQL query and creates a short description of the query. It is useful for logging.
|
||||
def operation_log(query, operation_name, variables)
|
||||
return "NoQuery" if query.nil?
|
||||
|
||||
operation = parse_graphql_query(query, operation_name)
|
||||
|
||||
return "InvalidQuery" if operation.nil?
|
||||
return "IntrospectionQuery" if operation.name == "IntrospectionQuery"
|
||||
|
||||
message = "#{operation.operation_type}: "
|
||||
message += if operation.name.present?
|
||||
"#{operation.name} { "
|
||||
else
|
||||
"{ "
|
||||
end
|
||||
message += operation.selections.map(&:name).join(', ')
|
||||
message += " } "
|
||||
message += if variables.present?
|
||||
variables.flat_map do |(name, value)|
|
||||
format_graphql_variable(name, value)
|
||||
end
|
||||
else
|
||||
operation.selections.flat_map(&:arguments).flat_map do |argument|
|
||||
format_graphql_variable(argument.name, argument.value)
|
||||
end
|
||||
end.join(', ')
|
||||
|
||||
message.strip
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_graphql_query(query, operation_name)
|
||||
operations = GraphQL.parse(query).children.filter do |node|
|
||||
node.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
||||
end
|
||||
if operations.size == 1
|
||||
operations.first
|
||||
else
|
||||
operations.find { |node| node.name == operation_name }
|
||||
end
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
def format_graphql_variable(name, value)
|
||||
if value.is_a?(Hash)
|
||||
value.map do |(name, value)|
|
||||
format_graphql_variable(name, value)
|
||||
end
|
||||
elsif value.is_a?(GraphQL::Language::Nodes::InputObject)
|
||||
value.arguments.map do |argument|
|
||||
format_graphql_variable(argument.name, argument.value)
|
||||
end
|
||||
else
|
||||
"#{name}: \"#{value.to_s.truncate(10)}\""
|
||||
end
|
||||
end
|
||||
end
|
9
app/models/agent_connect_client.rb
Normal file
9
app/models/agent_connect_client.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
class AgentConnectClient < OpenIDConnect::Client
|
||||
def initialize(code = nil)
|
||||
super(AGENT_CONNECT)
|
||||
|
||||
if code.present?
|
||||
self.authorization_code = code
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,7 +14,6 @@
|
|||
class AttestationTemplate < ApplicationRecord
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include TagsSubstitutionConcern
|
||||
include FileValidationConcern
|
||||
|
||||
belongs_to :procedure, optional: false
|
||||
|
||||
|
@ -24,8 +23,8 @@ class AttestationTemplate < ApplicationRecord
|
|||
validates :footer, length: { maximum: 190 }
|
||||
|
||||
FILE_MAX_SIZE = 1.megabytes
|
||||
validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: file_size_validation(FILE_MAX_SIZE)
|
||||
validates :signature, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: file_size_validation(FILE_MAX_SIZE)
|
||||
validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: FILE_MAX_SIZE }
|
||||
validates :signature, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: FILE_MAX_SIZE }
|
||||
|
||||
DOSSIER_STATE = Dossier.states.fetch(:accepte)
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
#
|
||||
class Avis < ApplicationRecord
|
||||
include EmailSanitizableConcern
|
||||
include FileValidationConcern
|
||||
|
||||
belongs_to :dossier, inverse_of: :avis, touch: true, optional: false
|
||||
belongs_to :experts_procedure, optional: false
|
||||
|
@ -31,16 +30,16 @@ class Avis < ApplicationRecord
|
|||
FILE_MAX_SIZE = 20.megabytes
|
||||
validates :piece_justificative_file,
|
||||
content_type: AUTHORIZED_CONTENT_TYPES,
|
||||
size: file_size_validation(FILE_MAX_SIZE)
|
||||
size: { less_than: FILE_MAX_SIZE }
|
||||
|
||||
validates :introduction_file,
|
||||
content_type: AUTHORIZED_CONTENT_TYPES,
|
||||
size: file_size_validation(FILE_MAX_SIZE)
|
||||
size: { less_than: FILE_MAX_SIZE }
|
||||
|
||||
validates :email, format: { with: Devise.email_regexp, message: "n'est pas valide" }, allow_nil: true
|
||||
validates :claimant, presence: true
|
||||
validates :piece_justificative_file, size: file_size_validation(FILE_MAX_SIZE)
|
||||
validates :introduction_file, size: file_size_validation(FILE_MAX_SIZE)
|
||||
validates :piece_justificative_file, size: { less_than: FILE_MAX_SIZE }
|
||||
validates :introduction_file, size: { less_than: FILE_MAX_SIZE }
|
||||
before_validation -> { sanitize_email(:email) }
|
||||
|
||||
default_scope { joins(:dossier) }
|
||||
|
|
|
@ -20,11 +20,10 @@
|
|||
# type_de_champ_id :integer
|
||||
#
|
||||
class Champs::PieceJustificativeChamp < Champ
|
||||
include FileValidationConcern
|
||||
FILE_MAX_SIZE = 200.megabytes
|
||||
|
||||
validates :piece_justificative_file,
|
||||
size: file_size_validation(FILE_MAX_SIZE),
|
||||
size: { less_than: FILE_MAX_SIZE },
|
||||
if: -> { !type_de_champ.skip_pj_validation }
|
||||
|
||||
validates :piece_justificative_file,
|
||||
|
|
|
@ -20,20 +20,9 @@
|
|||
# type_de_champ_id :integer
|
||||
#
|
||||
class Champs::TitreIdentiteChamp < Champ
|
||||
include FileValidationConcern
|
||||
FILE_MAX_SIZE = 20.megabytes
|
||||
|
||||
ACCEPTED_FORMATS = [
|
||||
"image/png",
|
||||
"image/jpeg"
|
||||
]
|
||||
|
||||
# TODO: once we're running on Rails 6, re-enable this validation.
|
||||
# See https://github.com/betagouv/demarches-simplifiees.fr/issues/4926
|
||||
#
|
||||
validates :piece_justificative_file,
|
||||
content_type: ACCEPTED_FORMATS,
|
||||
size: file_size_validation(FILE_MAX_SIZE)
|
||||
ACCEPTED_FORMATS = ['image/png', 'image/jpeg']
|
||||
validates :piece_justificative_file, content_type: ACCEPTED_FORMATS, size: { less_than: FILE_MAX_SIZE }
|
||||
|
||||
def main_value_name
|
||||
:piece_justificative_file
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
# instructeur_id :bigint
|
||||
#
|
||||
class Commentaire < ApplicationRecord
|
||||
include FileValidationConcern
|
||||
include Discard::Model
|
||||
|
||||
self.ignored_columns = [:user_id]
|
||||
|
@ -31,7 +30,7 @@ class Commentaire < ApplicationRecord
|
|||
FILE_MAX_SIZE = 20.megabytes
|
||||
validates :piece_jointe,
|
||||
content_type: AUTHORIZED_CONTENT_TYPES,
|
||||
size: file_size_validation(FILE_MAX_SIZE)
|
||||
size: { less_than: FILE_MAX_SIZE }
|
||||
|
||||
default_scope { order(created_at: :asc) }
|
||||
scope :updated_since?, -> (date) { where('commentaires.updated_at > ?', date) }
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
module FileValidationConcern
|
||||
extend ActiveSupport::Concern
|
||||
class_methods do
|
||||
# This method works around missing `%{min_size}` and `%{max_size}` variables in active_record_validation
|
||||
# default error message.
|
||||
#
|
||||
# Hopefully this will be fixed upstream in https://github.com/igorkasyanchuk/active_storage_validations/pull/134
|
||||
def file_size_validation(file_max_size = 200.megabytes)
|
||||
{ less_than: file_max_size, message: I18n.t('errors.messages.file_size_out_of_range', file_size_limit: ActiveSupport::NumberHelper.number_to_human_size(file_max_size)) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,6 +8,7 @@
|
|||
# login_token_created_at :datetime
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# agent_connect_id :string
|
||||
#
|
||||
class Instructeur < ApplicationRecord
|
||||
has_many :administrateurs_instructeurs
|
||||
|
|
|
@ -54,7 +54,6 @@ class Procedure < ApplicationRecord
|
|||
self.ignored_columns = [:duree_conservation_dossiers_hors_ds]
|
||||
include ProcedureStatsConcern
|
||||
include EncryptableConcern
|
||||
include FileValidationConcern
|
||||
|
||||
include Discard::Model
|
||||
self.discard_column = :hidden_at
|
||||
|
@ -235,7 +234,6 @@ class Procedure < ApplicationRecord
|
|||
validates :description, presence: true, allow_blank: false, allow_nil: false
|
||||
validates :administrateurs, presence: true
|
||||
validates :lien_site_web, presence: true, if: :publiee?
|
||||
validate :validate_for_publication, on: :publication
|
||||
validate :check_juridique
|
||||
validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,200}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false }
|
||||
validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION }
|
||||
|
@ -254,7 +252,7 @@ class Procedure < ApplicationRecord
|
|||
"image/jpg",
|
||||
"image/png",
|
||||
"text/plain"
|
||||
], size: file_size_validation(FILE_MAX_SIZE), if: -> { new_record? || created_at > Date.new(2020, 2, 28) }
|
||||
], size: { less_than: FILE_MAX_SIZE }, if: -> { new_record? || created_at > Date.new(2020, 2, 28) }
|
||||
|
||||
validates :deliberation, content_type: [
|
||||
"application/msword",
|
||||
|
@ -265,11 +263,11 @@ class Procedure < ApplicationRecord
|
|||
"image/jpg",
|
||||
"image/png",
|
||||
"text/plain"
|
||||
], size: file_size_validation(FILE_MAX_SIZE), if: -> { new_record? || created_at > Date.new(2020, 4, 29) }
|
||||
], size: { less_than: FILE_MAX_SIZE }, if: -> { new_record? || created_at > Date.new(2020, 4, 29) }
|
||||
|
||||
LOGO_MAX_SIZE = 5.megabytes
|
||||
validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'],
|
||||
size: file_size_validation(LOGO_MAX_SIZE),
|
||||
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
|
||||
|
@ -325,18 +323,6 @@ class Procedure < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def validate_for_publication
|
||||
old_attributes = self.slice(:aasm_state, :closed_at)
|
||||
self.aasm_state = :publiee
|
||||
self.closed_at = nil
|
||||
|
||||
is_valid = validate
|
||||
|
||||
self.attributes = old_attributes
|
||||
|
||||
is_valid
|
||||
end
|
||||
|
||||
def suggested_path(administrateur)
|
||||
if path_customized?
|
||||
return path
|
||||
|
|
24
app/services/agent_connect_service.rb
Normal file
24
app/services/agent_connect_service.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
class AgentConnectService
|
||||
def self.enabled?
|
||||
ENV.fetch("AGENT_CONNECT_ENABLED", "enabled") == "enabled"
|
||||
end
|
||||
|
||||
def self.authorization_uri
|
||||
client = AgentConnectClient.new
|
||||
|
||||
client.authorization_uri(
|
||||
scope: [:openid, :email],
|
||||
state: SecureRandom.hex(16),
|
||||
nonce: SecureRandom.hex(16),
|
||||
acr_values: 'eidas1'
|
||||
)
|
||||
end
|
||||
|
||||
def self.user_info(code)
|
||||
client = AgentConnectClient.new(code)
|
||||
|
||||
client.access_token!(client_auth_method: :secret)
|
||||
.userinfo!
|
||||
.raw_attributes
|
||||
end
|
||||
end
|
8
app/views/agent_connect/agent/index.html.haml
Normal file
8
app/views/agent_connect/agent/index.html.haml
Normal file
|
@ -0,0 +1,8 @@
|
|||
- content_for(:title, t('.cta'))
|
||||
|
||||
.container
|
||||
%h1.mt-2.mb-2= t('.connect')
|
||||
|
||||
%p= t('.intro_html', app_name: APPLICATION_NAME)
|
||||
|
||||
= link_to t('.cta'), agent_connect_login_path, class: "france-connect-agent-login-button"
|
|
@ -30,14 +30,5 @@
|
|||
autocomplete: 'off',
|
||||
placeholder: 'https://exemple.gouv.fr/ma_demarche')
|
||||
|
||||
- procedure.validate(:publication)
|
||||
- errors = procedure.errors
|
||||
-# Ignore the :taken error if the path can be claimed
|
||||
- if errors.details[:path]&.pluck(:error)&.include?(:taken) && procedure.path_available?(administrateur, procedure.path)
|
||||
- errors.delete(:path)
|
||||
|
||||
- options = { class: "button primary", id: 'publish' }
|
||||
- if errors.details[:path].present?
|
||||
- options[:disabled] = :disabled
|
||||
.flex.justify-end
|
||||
= submit_tag procedure_publish_label(procedure, :submit), options
|
||||
= submit_tag procedure_publish_label(procedure, :submit), { class: "button primary", id: 'publish' }
|
||||
|
|
|
@ -23,7 +23,13 @@
|
|||
|
||||
= f.submit t('views.users.sessions.new.connection'), class: "button large primary expand"
|
||||
|
||||
%hr
|
||||
.france-connect-login-separator
|
||||
= t('views.shared.france_connect_login.separator')
|
||||
- if AgentConnectService.enabled?
|
||||
.center
|
||||
%p.mb-2= t('views.users.sessions.new.instructor_or_admin')
|
||||
= link_to t('views.users.sessions.new.connect_with_agent_connect'), agent_connect_path
|
||||
%hr
|
||||
%p.center
|
||||
%span= t('views.users.sessions.new.are_you_new', app_name: APPLICATION_NAME.gsub("-","‑")).html_safe
|
||||
%br
|
||||
|
|
|
@ -45,6 +45,13 @@ FC_PARTICULIER_ID=""
|
|||
FC_PARTICULIER_SECRET=""
|
||||
FC_PARTICULIER_BASE_URL=""
|
||||
|
||||
# Service externe: authentification Agent Connect
|
||||
AGENT_CONNECT_ID=""
|
||||
AGENT_CONNECT_SECRET=""
|
||||
AGENT_CONNECT_BASE_URL=""
|
||||
AGENT_CONNECT_JWKS=""
|
||||
AGENT_CONNECT_REDIRECT=""
|
||||
|
||||
# Service externe: Support Utilisateur HelpScout | Spécifique démarches-simplifiées.fr
|
||||
HELPSCOUT_MAILBOX_ID=""
|
||||
HELPSCOUT_CLIENT_ID=""
|
||||
|
|
|
@ -11,6 +11,9 @@ DS_ENV="staging"
|
|||
# Utilisation de France Connect
|
||||
# FRANCE_CONNECT_ENABLED="disabled" # "enabled" par défaut
|
||||
|
||||
# Utilisation de Agent Connect
|
||||
# AGENT_CONNECT_ENABLED="disabled" # "enabled" par défaut
|
||||
|
||||
# Personnalisation d'instance - URLs des CGU et des mentions légales
|
||||
# CGU_URL=""
|
||||
# MENTIONS_LEGALES_URL=""
|
||||
|
|
|
@ -101,6 +101,7 @@ ignore_unused:
|
|||
- 'activerecord.errors.*'
|
||||
- 'errors.messages.blank'
|
||||
- 'errors.messages.content_type_invalid'
|
||||
- 'errors.messages.file_size_out_of_range'
|
||||
- 'pluralize.*'
|
||||
- 'views.pagination.*'
|
||||
- 'time.formats.default'
|
||||
|
|
1
config/initializers/agent_connect.rb
Normal file
1
config/initializers/agent_connect.rb
Normal file
|
@ -0,0 +1 @@
|
|||
AGENT_CONNECT = Rails.application.secrets.agent_connect
|
|
@ -3,3 +3,59 @@ OpenIDConnect.logger = Rails.logger
|
|||
Rack::OAuth2.logger = Rails.logger
|
||||
# Webfinger.logger = Rails.logger
|
||||
SWD.logger = Rails.logger
|
||||
|
||||
# the openid_connect gem does not support
|
||||
# jwt format in the userinfo call.
|
||||
# A PR is open to improve the situation
|
||||
# https://github.com/nov/openid_connect/pull/54
|
||||
module OpenIDConnect
|
||||
class AccessToken < Rack::OAuth2::AccessToken::Bearer
|
||||
private
|
||||
|
||||
def jwk_loader
|
||||
JSON.parse(URI.parse(ENV['AGENT_CONNECT_JWKS']).read).deep_symbolize_keys
|
||||
end
|
||||
|
||||
def decode_jwt(requested_host, jwt)
|
||||
agent_connect_host = URI.parse(ENV['AGENT_CONNECT_BASE_URL']).host
|
||||
|
||||
if requested_host == agent_connect_host
|
||||
# rubocop:disable Lint/UselessAssignment
|
||||
JWT.decode(jwt, key = nil, verify = true, { algorithms: ['ES256'], jwks: jwk_loader })[0]
|
||||
# rubocop:enable Lint/UselessAssignment
|
||||
else
|
||||
raise "unknwon host : #{requested_host}"
|
||||
end
|
||||
end
|
||||
|
||||
def resource_request
|
||||
res = yield
|
||||
case res.status
|
||||
when 200
|
||||
hash = case parse_type_and_subtype(res.content_type)
|
||||
when 'application/jwt'
|
||||
requested_host = URI.parse(client.userinfo_endpoint).host
|
||||
decode_jwt(requested_host, res.body)
|
||||
when 'application/json'
|
||||
JSON.parse(res.body)
|
||||
end
|
||||
hash&.with_indifferent_access
|
||||
when 400
|
||||
raise BadRequest.new('API Access Faild', res)
|
||||
when 401
|
||||
raise Unauthorized.new('Access Token Invalid or Expired', res)
|
||||
when 403
|
||||
raise Forbidden.new('Insufficient Scope', res)
|
||||
else
|
||||
raise HttpError.new(res.status, 'Unknown HttpError', res)
|
||||
end
|
||||
end
|
||||
|
||||
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
|
||||
# - type and subtype are the first member
|
||||
# they are case insensitive
|
||||
def parse_type_and_subtype(content_type)
|
||||
content_type.split(';')[0].strip.downcase
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
en:
|
||||
errors:
|
||||
messages:
|
||||
content_type_invalid: "is not of an accepted type"
|
||||
file_size_out_of_range: "is too big. The file must be at most %{file_size_limit}."
|
||||
content_type_invalid: is not of an accepted type
|
||||
file_size_out_of_range: is too big. The file must be at most %{max_size}.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
fr:
|
||||
errors:
|
||||
messages:
|
||||
content_type_invalid: "n’est pas d’un type accepté"
|
||||
file_size_out_of_range: "est trop lourd(e). Le fichier doit faire au plus %{file_size_limit}."
|
||||
content_type_invalid: n’est pas d’un type accepté
|
||||
file_size_out_of_range: est trop lourd(e). Le fichier doit faire au plus %{max_size}.
|
||||
|
|
|
@ -204,6 +204,8 @@ en:
|
|||
connection: Sign in
|
||||
are_you_new: First time on %{app_name}?
|
||||
find_procedure: Find your procedure
|
||||
instructor_or_admin: Instructor or Administrator ?
|
||||
connect_with_agent_connect: Connect with AgentConnect
|
||||
passwords:
|
||||
reset_link_sent:
|
||||
got_it: Got it!
|
||||
|
|
|
@ -200,6 +200,8 @@ fr:
|
|||
connection: Se connecter
|
||||
are_you_new: Vous êtes nouveau sur %{app_name} ?
|
||||
find_procedure: Trouvez votre démarche
|
||||
instructor_or_admin: Vous êtes instructeur ou administrateur ?
|
||||
connect_with_agent_connect: Se connecter avec AgentConnect
|
||||
passwords:
|
||||
reset_link_sent:
|
||||
email_sent_html: "Nous vous avons envoyé un email à l’adresse <strong>%{email}</strong>."
|
||||
|
|
11
config/locales/views/agent_connect/agent/en.yml
Normal file
11
config/locales/views/agent_connect/agent/en.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
en:
|
||||
agent_connect:
|
||||
agent:
|
||||
index:
|
||||
connect: Connect with AgentConnect
|
||||
intro_html: |
|
||||
AgentConnect allows <b class='bold'>instructors et administrators</b> to use their usual login credentials to connect to %{app_name}.
|
||||
<br />
|
||||
<br />
|
||||
Only agents of <b class='bold'>the Ministry of Ecological Transition</b> can currently benefit from it.
|
||||
cta: Connect with AgentConnect
|
11
config/locales/views/agent_connect/agent/fr.yml
Normal file
11
config/locales/views/agent_connect/agent/fr.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
fr:
|
||||
agent_connect:
|
||||
agent:
|
||||
index:
|
||||
connect: Connectez-vous avec AgentConnect
|
||||
intro_html: |
|
||||
AgentConnect permet aux <b class='bold'>instructeurs et administrateurs</b> d’utiliser leurs identifiants habituels pour se connecter à %{app_name}.
|
||||
<br />
|
||||
<br />
|
||||
Seul les agents du <b class='bold'>ministère de la Transition écologique</b> peuvent actuellement en bénéficier.
|
||||
cta: S’identifier avec AgentConnect
|
|
@ -129,6 +129,12 @@ Rails.application.routes.draw do
|
|||
post 'particulier/merge_with_new_account' => 'particulier#merge_with_new_account'
|
||||
end
|
||||
|
||||
namespace :agent_connect do
|
||||
get '' => 'agent#index'
|
||||
get 'login' => 'agent#login'
|
||||
get 'callback' => 'agent#callback'
|
||||
end
|
||||
|
||||
namespace :champs do
|
||||
get ':position/siret', to: 'siret#show', as: :siret
|
||||
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link
|
||||
|
|
|
@ -25,6 +25,14 @@ defaults: &defaults
|
|||
token_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/token
|
||||
userinfo_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/userinfo
|
||||
logout_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/logout
|
||||
agent_connect:
|
||||
identifier: <%= ENV['AGENT_CONNECT_ID'] %>
|
||||
secret: <%= ENV['AGENT_CONNECT_SECRET'] %>
|
||||
redirect_uri: <%= ENV['AGENT_CONNECT_REDIRECT'] %>
|
||||
authorization_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/authorize
|
||||
token_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/token
|
||||
userinfo_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/userinfo
|
||||
logout_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/session/end
|
||||
mailjet:
|
||||
api_key: <%= ENV['MAILJET_API_KEY'] %>
|
||||
secret_key: <%= ENV['MAILJET_SECRET_KEY'] %>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class AddAgentConnectSubColumnToInstructeursTable < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :instructeurs, :agent_connect_id, :string
|
||||
add_index :instructeurs, :agent_connect_id, unique: true
|
||||
end
|
||||
end
|
|
@ -10,7 +10,8 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2021_11_15_112933) do
|
||||
ActiveRecord::Schema.define(version: 2021_11_19_112046) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
enable_extension "unaccent"
|
||||
|
@ -534,6 +535,8 @@ ActiveRecord::Schema.define(version: 2021_11_15_112933) do
|
|||
t.text "encrypted_login_token"
|
||||
t.datetime "login_token_created_at"
|
||||
t.boolean "bypass_email_login_token", default: false, null: false
|
||||
t.string "agent_connect_id"
|
||||
t.index ["agent_connect_id"], name: "index_instructeurs_on_agent_connect_id", unique: true
|
||||
end
|
||||
|
||||
create_table "invites", id: :serial, force: :cascade do |t|
|
||||
|
|
90
spec/controllers/agent_connect/agent_controller_spec.rb
Normal file
90
spec/controllers/agent_connect/agent_controller_spec.rb
Normal file
|
@ -0,0 +1,90 @@
|
|||
describe AgentConnect::AgentController, type: :controller do
|
||||
describe '#callback' do
|
||||
let(:email) { 'i@email.com' }
|
||||
subject { get :callback, params: { code: code } }
|
||||
|
||||
context 'when the callback code is correct' do
|
||||
let(:code) { 'correct' }
|
||||
let(:user_info) { { 'sub' => 'sub', 'email' => ' I@email.com' } }
|
||||
|
||||
context 'and user_info returns some info' do
|
||||
before do
|
||||
expect(AgentConnectService).to receive(:user_info).and_return(user_info)
|
||||
end
|
||||
|
||||
context 'and the instructeur does not have an account yet' do
|
||||
before do
|
||||
expect(controller).to receive(:sign_in)
|
||||
end
|
||||
|
||||
it 'creates the user, signs in and redirects to procedure_path' do
|
||||
expect { subject }.to change { User.count }.by(1).and change { Instructeur.count }.by(1)
|
||||
|
||||
last_user = User.last
|
||||
|
||||
expect(last_user.email).to eq(email)
|
||||
expect(last_user.confirmed_at).to be_present
|
||||
expect(last_user.instructeur.agent_connect_id).to eq('sub')
|
||||
expect(response).to redirect_to(instructeur_procedures_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and the instructeur already has an account' do
|
||||
let!(:instructeur) { create(:instructeur, email: email) }
|
||||
|
||||
before do
|
||||
expect(controller).to receive(:sign_in)
|
||||
end
|
||||
|
||||
it 'reuses the account, signs in and redirects to procedure_path' do
|
||||
expect { subject }.to change { User.count }.by(0).and change { Instructeur.count }.by(0)
|
||||
|
||||
instructeur.reload
|
||||
|
||||
expect(instructeur.agent_connect_id).to eq('sub')
|
||||
expect(response).to redirect_to(instructeur_procedures_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and the instructeur already has an account as a user' do
|
||||
let!(:user) { create(:user, email: email) }
|
||||
|
||||
before do
|
||||
expect(controller).to receive(:sign_in)
|
||||
end
|
||||
|
||||
it 'reuses the account, signs in and redirects to procedure_path' do
|
||||
expect { subject }.to change { User.count }.by(0).and change { Instructeur.count }.by(1)
|
||||
|
||||
instructeur = user.reload.instructeur
|
||||
|
||||
expect(instructeur.agent_connect_id).to eq('sub')
|
||||
expect(response).to redirect_to(instructeur_procedures_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'but user_info raises and error' do
|
||||
before do
|
||||
expect(AgentConnectService).to receive(:user_info).and_raise(Rack::OAuth2::Client::Error.new(500, error: 'Unknown'))
|
||||
end
|
||||
|
||||
it 'aborts the processus' do
|
||||
expect { subject }.to change { User.count }.by(0).and change { Instructeur.count }.by(0)
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the callback code is blank' do
|
||||
let(:code) { '' }
|
||||
|
||||
it 'aborts the processus' do
|
||||
expect { subject }.to change { User.count }.by(0).and change { Instructeur.count }.by(0)
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
RSpec.describe GraphqlOperationLogConcern, type: :controller do
|
||||
class TestController < ActionController::Base
|
||||
include GraphqlOperationLogConcern
|
||||
end
|
||||
|
||||
controller TestController do
|
||||
end
|
||||
|
||||
describe '#operation_log' do
|
||||
let(:query) { nil }
|
||||
let(:variables) { nil }
|
||||
let(:operation_name) { nil }
|
||||
|
||||
subject { controller.operation_log(query, operation_name, variables) }
|
||||
|
||||
context 'with no query' do
|
||||
it { expect(subject).to eq('NoQuery') }
|
||||
end
|
||||
|
||||
context 'with invalid query' do
|
||||
let(:query) { 'query { demarche {} }' }
|
||||
|
||||
it { expect(subject).to eq('InvalidQuery') }
|
||||
end
|
||||
|
||||
context 'with two queries' do
|
||||
let(:query) { 'query demarche { demarche } query dossier { dossier }' }
|
||||
let(:operation_name) { 'dossier' }
|
||||
|
||||
it { expect(subject).to eq('query: dossier { dossier }') }
|
||||
end
|
||||
|
||||
context 'with arguments' do
|
||||
let(:query) { 'query demarche { demarche(number: 123) { id } }' }
|
||||
|
||||
it { expect(subject).to eq('query: demarche { demarche } number: "123"') }
|
||||
end
|
||||
|
||||
context 'with variables' do
|
||||
let(:query) { 'query { demarche(number: 123) { id } }' }
|
||||
let(:variables) { { number: 124 } }
|
||||
|
||||
it { expect(subject).to eq('query: { demarche } number: "124"') }
|
||||
end
|
||||
|
||||
context 'with mutation and arguments' do
|
||||
let(:query) { 'mutation { passerDossierEnInstruction(input: { number: 123 }) { id } }' }
|
||||
|
||||
it { expect(subject).to eq('mutation: { passerDossierEnInstruction } number: "123"') }
|
||||
end
|
||||
|
||||
context 'with mutation and variables' do
|
||||
let(:query) { 'mutation { passerDossierEnInstruction(input: { number: 123 }) { id } }' }
|
||||
let(:variables) { { input: { number: 124 } } }
|
||||
|
||||
it { expect(subject).to eq('mutation: { passerDossierEnInstruction } number: "124"') }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -131,7 +131,7 @@ describe NewAdministrateur::ServicesController, type: :controller do
|
|||
end
|
||||
|
||||
it { expect(service.reload).not_to be_nil }
|
||||
it { expect(flash.alert).to eq("la démarche #{procedure.libelle} utilise encore le service service. Veuillez l'affecter à un autre service avant de pouvoir le supprimer") }
|
||||
it { expect(flash.alert).to eq("la démarche #{procedure.libelle} utilise encore le service #{service.nom}. Veuillez l'affecter à un autre service avant de pouvoir le supprimer") }
|
||||
it { expect(flash.notice).to be_nil }
|
||||
it { expect(response).to redirect_to(admin_services_path(procedure_id: 12)) }
|
||||
end
|
||||
|
|
|
@ -1,15 +1,35 @@
|
|||
describe RechercheController, type: :controller do
|
||||
let(:dossier) { create(:dossier, :en_construction, :with_populated_annotations) }
|
||||
let(:dossier2) { create(:dossier, :en_construction, procedure: dossier.procedure) }
|
||||
let(:procedure) {
|
||||
create(:procedure,
|
||||
:published,
|
||||
:for_individual,
|
||||
:with_type_de_champ,
|
||||
:with_type_de_champ_private,
|
||||
types_de_champ_count: 2,
|
||||
types_de_champ_private_count: 2)
|
||||
}
|
||||
let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure: procedure) }
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
|
||||
let(:dossier_with_expert) { avis.dossier }
|
||||
let(:avis) { create(:avis, dossier: create(:dossier, :en_construction, :with_populated_annotations)) }
|
||||
let(:dossier_with_expert) { create(:dossier, :en_construction, :with_individual, procedure: procedure) }
|
||||
let(:avis) { create(:avis, dossier: dossier_with_expert) }
|
||||
|
||||
let(:user) { instructeur.user }
|
||||
|
||||
before do
|
||||
instructeur.assign_to_procedure(dossier.procedure)
|
||||
|
||||
dossier.champs[0].value = "Name of district A"
|
||||
dossier.champs[1].value = "Name of city A"
|
||||
dossier.champs_private[0].value = "Dossier A is complete"
|
||||
dossier.champs_private[1].value = "Dossier A is valid"
|
||||
dossier.save!
|
||||
|
||||
dossier_with_expert.champs[0].value = "Name of district B"
|
||||
dossier_with_expert.champs[1].value = "name of city B"
|
||||
dossier_with_expert.champs_private[0].value = "Dossier B is incomplete"
|
||||
dossier_with_expert.champs_private[1].value = "Dossier B is invalid"
|
||||
dossier_with_expert.save!
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
|
@ -46,8 +66,8 @@ describe RechercheController, type: :controller do
|
|||
end
|
||||
|
||||
context 'when instructeur do not own the dossier' do
|
||||
let(:dossier3) { create(:dossier, :en_construction) }
|
||||
let(:query) { dossier3.id }
|
||||
let(:dossier2) { create(:dossier, :en_construction) }
|
||||
let(:query) { dossier2.id }
|
||||
|
||||
it { is_expected.to have_http_status(200) }
|
||||
|
||||
|
@ -69,29 +89,49 @@ describe RechercheController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'by private annotations' do
|
||||
context 'when instructeur search by private annotations' do
|
||||
let(:query) { dossier.private_search_terms }
|
||||
describe 'by champs' do
|
||||
let(:query) { 'district A' }
|
||||
|
||||
before { subject }
|
||||
before { subject }
|
||||
|
||||
it { is_expected.to have_http_status(200) }
|
||||
it { is_expected.to have_http_status(200) }
|
||||
|
||||
it 'returns the expected dossier' do
|
||||
expect(assigns(:projected_dossiers).count).to eq(1)
|
||||
expect(assigns(:projected_dossiers).first.dossier_id).to eq(dossier.id)
|
||||
end
|
||||
it 'returns the expected dossier' do
|
||||
expect(assigns(:projected_dossiers).count).to eq(1)
|
||||
expect(assigns(:projected_dossiers).first.dossier_id).to eq(dossier.id)
|
||||
end
|
||||
|
||||
context 'when expert search by private annotations' do
|
||||
context 'as an expert' do
|
||||
let(:user) { avis.experts_procedure.expert.user }
|
||||
let(:query) { dossier_with_expert.private_search_terms }
|
||||
|
||||
before { subject }
|
||||
let(:query) { 'district' }
|
||||
|
||||
it { is_expected.to have_http_status(200) }
|
||||
|
||||
it 'returns 0 dossiers' do
|
||||
it 'returns only the dossier available to the expert' do
|
||||
expect(assigns(:projected_dossiers).count).to eq(1)
|
||||
expect(assigns(:projected_dossiers).first.dossier_id).to eq(dossier_with_expert.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'by private annotations' do
|
||||
let(:query) { 'invalid' }
|
||||
|
||||
before { subject }
|
||||
|
||||
it { is_expected.to have_http_status(200) }
|
||||
|
||||
it 'returns the expected dossier' do
|
||||
expect(assigns(:projected_dossiers).count).to eq(1)
|
||||
expect(assigns(:projected_dossiers).first.dossier_id).to eq(dossier_with_expert.id)
|
||||
end
|
||||
|
||||
context 'as an expert' do
|
||||
let(:user) { avis.experts_procedure.expert.user }
|
||||
|
||||
it { is_expected.to have_http_status(200) }
|
||||
|
||||
it 'does not allow experts to search in private annotations' do
|
||||
expect(assigns(:projected_dossiers).count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -190,7 +190,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
factory :champ_siret, class: 'Champs::SiretChamp' do
|
||||
association :type_de_champ, factory: [:type_de_champ_siret]
|
||||
type_de_champ { association :type_de_champ_siret, procedure: dossier.procedure }
|
||||
association :etablissement, factory: [:etablissement]
|
||||
value { '44011762001530' }
|
||||
end
|
||||
|
|
|
@ -2,27 +2,19 @@ FactoryBot.define do
|
|||
factory :dossier do
|
||||
autorisation_donnees { true }
|
||||
state { Dossier.states.fetch(:brouillon) }
|
||||
association :user
|
||||
|
||||
user { association :user }
|
||||
groupe_instructeur { procedure.routee? ? nil : procedure.defaut_groupe_instructeur }
|
||||
revision { procedure.active_revision }
|
||||
individual { association(:individual, :empty, dossier: instance, strategy: :build) if procedure.for_individual? }
|
||||
|
||||
transient do
|
||||
procedure { nil }
|
||||
end
|
||||
|
||||
after(:build) do |dossier, evaluator|
|
||||
if evaluator.procedure.present?
|
||||
procedure = evaluator.procedure
|
||||
else
|
||||
procedure = create(:procedure, :published, :with_type_de_champ, :with_type_de_champ_private)
|
||||
end
|
||||
|
||||
dossier.revision = procedure.active_revision
|
||||
|
||||
# Assign the procedure to the dossier through the groupe_instructeur
|
||||
if dossier.groupe_instructeur.nil?
|
||||
dossier.groupe_instructeur = procedure.routee? ? nil : procedure.defaut_groupe_instructeur
|
||||
end
|
||||
|
||||
dossier.build_default_individual
|
||||
for_individual? { false }
|
||||
# For now a dossier must use a `create`d procedure, even if the dossier is only built (and not created).
|
||||
# This is because saving the dossier fails when the procedure has not been saved beforehand
|
||||
# (due to some internal ActiveRecord error).
|
||||
# TODO: find a way to find the issue and just `build` the procedure.
|
||||
procedure { create(:procedure, :published, :with_type_de_champ, :with_type_de_champ_private, for_individual: for_individual?) }
|
||||
end
|
||||
|
||||
trait :with_entreprise do
|
||||
|
@ -42,12 +34,11 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :with_individual do
|
||||
after(:build) do |dossier, evaluator|
|
||||
# If the procedure was implicitely created by the factory,
|
||||
# mark it automatically as for_individual.
|
||||
if evaluator.procedure.nil?
|
||||
dossier.procedure.update(for_individual: true)
|
||||
end
|
||||
transient do
|
||||
for_individual? { true }
|
||||
end
|
||||
|
||||
after(:build) do |dossier, _evaluator|
|
||||
if !dossier.procedure.for_individual?
|
||||
raise 'Inconsistent factory: attempting to create a dossier :with_individual on a procedure that is not `for_individual?`'
|
||||
end
|
||||
|
@ -99,9 +90,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :with_commentaires do
|
||||
after(:create) do |dossier, _evaluator|
|
||||
dossier.commentaires += create_list(:commentaire, 2)
|
||||
end
|
||||
commentaires { [build(:commentaire), build(:commentaire)] }
|
||||
end
|
||||
|
||||
trait :followed do
|
||||
|
|
|
@ -4,5 +4,9 @@ FactoryBot.define do
|
|||
factory :groupe_instructeur do
|
||||
label { generate(:groupe_label) }
|
||||
association :procedure
|
||||
|
||||
trait :default do
|
||||
label { GroupeInstructeur::DEFAUT_LABEL }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,5 +5,12 @@ FactoryBot.define do
|
|||
prenom { 'Xavier' }
|
||||
birthdate { Date.new(1991, 11, 01) }
|
||||
association :dossier
|
||||
|
||||
trait :empty do
|
||||
gender { nil }
|
||||
nom { nil }
|
||||
prenom { nil }
|
||||
birthdate { nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
FactoryBot.define do
|
||||
sequence(:published_path) { |n| "fake_path#{n}" }
|
||||
|
||||
factory :procedure do
|
||||
sequence(:libelle) { |n| "Procedure #{n}" }
|
||||
description { "Demande de subvention à l'intention des associations" }
|
||||
|
@ -12,6 +13,9 @@ FactoryBot.define do
|
|||
lien_site_web { "https://mon-site.gouv" }
|
||||
path { SecureRandom.uuid }
|
||||
|
||||
groupe_instructeurs { [association(:groupe_instructeur, :default, procedure: instance, strategy: :build)] }
|
||||
administrateurs { administrateur.present? ? [administrateur] : [association(:administrateur)] }
|
||||
|
||||
transient do
|
||||
administrateur { }
|
||||
instructeurs { [] }
|
||||
|
@ -21,29 +25,16 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
after(:build) do |procedure, evaluator|
|
||||
if evaluator.administrateur
|
||||
procedure.administrateurs = [evaluator.administrateur]
|
||||
elsif procedure.administrateurs.empty?
|
||||
procedure.administrateurs = [build(:administrateur)]
|
||||
end
|
||||
procedure.draft_revision = build(:procedure_revision, procedure: procedure)
|
||||
initial_revision = build(:procedure_revision, procedure: procedure)
|
||||
add_types_de_champs(evaluator.types_de_champ, to: initial_revision, scope: :public)
|
||||
add_types_de_champs(evaluator.types_de_champ_private, to: initial_revision, scope: :private)
|
||||
|
||||
evaluator.types_de_champ.each do |type_de_champ|
|
||||
type_de_champ.revision = procedure.draft_revision
|
||||
type_de_champ.private = false
|
||||
type_de_champ.revision.revision_types_de_champ << build(:procedure_revision_type_de_champ,
|
||||
revision: procedure.draft_revision,
|
||||
position: type_de_champ.order_place,
|
||||
type_de_champ: type_de_champ)
|
||||
end
|
||||
|
||||
evaluator.types_de_champ_private.each do |type_de_champ|
|
||||
type_de_champ.revision = procedure.draft_revision
|
||||
type_de_champ.private = true
|
||||
type_de_champ.revision.revision_types_de_champ_private << build(:procedure_revision_type_de_champ,
|
||||
revision: procedure.draft_revision,
|
||||
position: type_de_champ.order_place,
|
||||
type_de_champ: type_de_champ)
|
||||
if procedure.brouillon?
|
||||
procedure.draft_revision = initial_revision
|
||||
else
|
||||
procedure.published_revision = initial_revision
|
||||
procedure.published_revision.published_at = Time.zone.now
|
||||
procedure.draft_revision = build(:procedure_revision, from_original: initial_revision)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -71,11 +62,12 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
factory :simple_procedure do
|
||||
published
|
||||
|
||||
for_individual { true }
|
||||
|
||||
after(:build) do |procedure, _evaluator|
|
||||
procedure.for_individual = true
|
||||
build(:type_de_champ, libelle: 'Texte obligatoire', mandatory: true, procedure: procedure)
|
||||
procedure.path = generate(:published_path)
|
||||
procedure.publish!
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -88,9 +80,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :with_service do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
procedure.service = create(:service)
|
||||
end
|
||||
service { association :service, administrateur: administrateurs.first }
|
||||
end
|
||||
|
||||
trait :with_instructeur do
|
||||
|
@ -106,9 +96,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :for_individual do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
procedure.for_individual = true
|
||||
end
|
||||
for_individual { true }
|
||||
end
|
||||
|
||||
trait :with_auto_archive do
|
||||
|
@ -218,26 +206,27 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :published do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
procedure.path = generate(:published_path)
|
||||
procedure.publish!
|
||||
end
|
||||
aasm_state { :publiee }
|
||||
path { generate(:published_path) }
|
||||
published_at { Time.zone.now }
|
||||
unpublished_at { nil }
|
||||
closed_at { nil }
|
||||
end
|
||||
|
||||
trait :closed do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
procedure.path = generate(:published_path)
|
||||
procedure.publish!
|
||||
procedure.close!
|
||||
end
|
||||
published
|
||||
|
||||
aasm_state { :close }
|
||||
published_at { Time.zone.now - 1.second }
|
||||
closed_at { Time.zone.now }
|
||||
end
|
||||
|
||||
trait :unpublished do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
procedure.path = generate(:published_path)
|
||||
procedure.publish!
|
||||
procedure.unpublish!
|
||||
end
|
||||
published
|
||||
|
||||
aasm_state { :depubliee }
|
||||
published_at { Time.zone.now - 1.second }
|
||||
unpublished_at { Time.zone.now }
|
||||
end
|
||||
|
||||
trait :discarded do
|
||||
|
@ -308,3 +297,17 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_types_de_champs(types_de_champ, to: nil, scope: :public)
|
||||
revision = to
|
||||
association_name = scope == :private ? :revision_types_de_champ_private : :revision_types_de_champ
|
||||
|
||||
types_de_champ.each do |type_de_champ|
|
||||
type_de_champ.revision = revision
|
||||
type_de_champ.private = (scope == :private)
|
||||
type_de_champ.revision.public_send(association_name) << build(:procedure_revision_type_de_champ,
|
||||
revision: revision,
|
||||
position: type_de_champ.order_place,
|
||||
type_de_champ: type_de_champ)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,21 @@
|
|||
FactoryBot.define do
|
||||
factory :procedure_revision do
|
||||
transient do
|
||||
from_original { nil }
|
||||
end
|
||||
|
||||
after(:build) do |revision, evaluator|
|
||||
if evaluator.from_original
|
||||
original = evaluator.from_original
|
||||
|
||||
revision.procedure = original.procedure
|
||||
original.revision_types_de_champ.each do |r_tdc|
|
||||
revision.revision_types_de_champ << build(:procedure_revision_type_de_champ, from_original: r_tdc)
|
||||
end
|
||||
original.revision_types_de_champ_private.each do |r_tdc|
|
||||
revision.revision_types_de_champ_private << build(:procedure_revision_type_de_champ, from_original: r_tdc)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
FactoryBot.define do
|
||||
factory :procedure_revision_type_de_champ do
|
||||
transient do
|
||||
from_original { nil }
|
||||
end
|
||||
|
||||
after(:build) do |revision_type_de_champ, evaluator|
|
||||
if evaluator.from_original
|
||||
original = evaluator.from_original
|
||||
|
||||
revision_type_de_champ.type_de_champ = original.type_de_champ
|
||||
revision_type_de_champ.position = original.position
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
FactoryBot.define do
|
||||
factory :service do
|
||||
nom { 'service' }
|
||||
sequence(:nom) { |n| "Service #{n}" }
|
||||
organisme { 'organisme' }
|
||||
type_organisme { Service.type_organismes.fetch(:association) }
|
||||
email { 'email@toto.com' }
|
||||
|
|
|
@ -49,7 +49,7 @@ RSpec.describe DossierHelper, type: :helper do
|
|||
let(:procedure) { create(:simple_procedure, :for_individual) }
|
||||
|
||||
context "when the individual is not provided" do
|
||||
let(:individual) { nil }
|
||||
let(:individual) { build(:individual, :empty) }
|
||||
it { is_expected.to be_blank }
|
||||
end
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe APIEntreprise::Job, type: :job do
|
|||
describe '#perform' do
|
||||
let(:dossier) { create(:dossier, :with_entreprise) }
|
||||
|
||||
context 'when a un retryable error is raised' do
|
||||
context 'when an un-retriable error is raised' do
|
||||
let(:errors) { [:standard_error] }
|
||||
|
||||
it 'does not retry' do
|
||||
|
@ -14,7 +14,7 @@ RSpec.describe APIEntreprise::Job, type: :job do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when a retryable error is raised' do
|
||||
context 'when a retriable error is raised' do
|
||||
let(:errors) { [:service_unavaible, :bad_gateway, :timed_out] }
|
||||
|
||||
it 'retries 5 times' do
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
RSpec.describe ApplicationMailer, type: :mailer do
|
||||
describe 'dealing with invalid emails' do
|
||||
let(:dossier) { create(:dossier, procedure: build(:simple_procedure)) }
|
||||
let(:dossier) { create(:dossier, procedure: create(:simple_procedure)) }
|
||||
subject { DossierMailer.notify_new_draft(dossier) }
|
||||
|
||||
describe 'invalid emails are not sent' do
|
||||
|
|
|
@ -12,7 +12,7 @@ RSpec.describe DossierMailer, type: :mailer do
|
|||
end
|
||||
|
||||
describe '.notify_new_draft' do
|
||||
let(:dossier) { create(:dossier, procedure: build(:simple_procedure, :with_auto_archive)) }
|
||||
let(:dossier) { create(:dossier, procedure: create(:simple_procedure, :with_auto_archive)) }
|
||||
|
||||
subject { described_class.notify_new_draft(dossier) }
|
||||
|
||||
|
@ -27,7 +27,7 @@ RSpec.describe DossierMailer, type: :mailer do
|
|||
end
|
||||
|
||||
describe '.notify_new_answer with dossier brouillon' do
|
||||
let(:dossier) { create(:dossier, procedure: build(:simple_procedure)) }
|
||||
let(:dossier) { create(:dossier, procedure: create(:simple_procedure)) }
|
||||
let(:commentaire) { create(:commentaire, dossier: dossier) }
|
||||
subject { described_class.with(commentaire: commentaire).notify_new_answer }
|
||||
|
||||
|
@ -39,8 +39,9 @@ RSpec.describe DossierMailer, type: :mailer do
|
|||
end
|
||||
|
||||
describe '.notify_new_answer with dossier en construction' do
|
||||
let(:dossier) { create(:dossier, state: "en_construction", procedure: build(:simple_procedure)) }
|
||||
let(:dossier) { create(:dossier, :en_construction, procedure: create(:simple_procedure)) }
|
||||
let(:commentaire) { create(:commentaire, dossier: dossier) }
|
||||
|
||||
subject { described_class.with(commentaire: commentaire).notify_new_answer }
|
||||
|
||||
it { expect(subject.subject).to include("Nouveau message") }
|
||||
|
@ -51,7 +52,7 @@ RSpec.describe DossierMailer, type: :mailer do
|
|||
end
|
||||
|
||||
describe '.notify_new_answer with commentaire discarded' do
|
||||
let(:dossier) { create(:dossier, procedure: build(:simple_procedure)) }
|
||||
let(:dossier) { create(:dossier, procedure: create(:simple_procedure)) }
|
||||
let(:commentaire) { create(:commentaire, dossier: dossier, discarded_at: 2.minutes.ago) }
|
||||
|
||||
subject { described_class.with(commentaire: commentaire).notify_new_answer }
|
||||
|
@ -83,7 +84,7 @@ RSpec.describe DossierMailer, type: :mailer do
|
|||
end
|
||||
|
||||
describe '.notify_revert_to_instruction' do
|
||||
let(:dossier) { create(:dossier, procedure: build(:simple_procedure)) }
|
||||
let(:dossier) { create(:dossier, procedure: create(:simple_procedure)) }
|
||||
|
||||
subject { described_class.notify_revert_to_instruction(dossier) }
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
describe Champs::IbanChamp do
|
||||
describe '#valid?' do
|
||||
it do
|
||||
|
|
|
@ -250,11 +250,21 @@ describe Dossier do
|
|||
expect(dossier.champs.count).to eq(1)
|
||||
expect(dossier.champs_private.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_default_individual' do
|
||||
let(:dossier) { build(:dossier, procedure: procedure, user: user) }
|
||||
|
||||
subject do
|
||||
dossier.individual = nil
|
||||
dossier.build_default_individual
|
||||
end
|
||||
|
||||
context 'when the dossier belongs to a procedure for individuals' do
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ, for_individual: true) }
|
||||
let(:procedure) { create(:procedure, for_individual: true) }
|
||||
|
||||
it 'creates a default individual' do
|
||||
subject
|
||||
expect(dossier.individual).to be_present
|
||||
expect(dossier.individual.nom).to be_nil
|
||||
expect(dossier.individual.prenom).to be_nil
|
||||
|
@ -266,6 +276,7 @@ describe Dossier do
|
|||
let(:user) { build(:user, france_connect_information: france_connect_information) }
|
||||
|
||||
it 'fills the individual with the informations from France Connect' do
|
||||
subject
|
||||
expect(dossier.individual.nom).to eq('DUBOIS')
|
||||
expect(dossier.individual.prenom).to eq('Angela Claire Louise')
|
||||
expect(dossier.individual.gender).to eq(Individual::GENDER_FEMALE)
|
||||
|
@ -274,9 +285,10 @@ describe Dossier do
|
|||
end
|
||||
|
||||
context 'when the dossier belongs to a procedure for moral personas' do
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ, for_individual: false) }
|
||||
let(:procedure) { create(:procedure, for_individual: false) }
|
||||
|
||||
it 'doesn’t create a individual' do
|
||||
subject
|
||||
expect(dossier.individual).to be_nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -262,14 +262,14 @@ describe Instructeur, type: :model do
|
|||
end
|
||||
|
||||
describe '#notifications_for_groupe_instructeurs' do
|
||||
# one procedure, one group, 2 instructeurs
|
||||
# a procedure, one group, 2 instructeurs
|
||||
let(:procedure) { create(:simple_procedure, :routee, :with_type_de_champ_private, :for_individual) }
|
||||
let(:gi_p1) { procedure.groupe_instructeurs.last }
|
||||
let!(:dossier) { create(:dossier, :with_individual, :followed, groupe_instructeur: gi_p1, state: Dossier.states.fetch(:en_construction)) }
|
||||
let!(:dossier) { create(:dossier, :with_individual, :followed, procedure: procedure, groupe_instructeur: gi_p1, state: Dossier.states.fetch(:en_construction)) }
|
||||
let(:instructeur) { dossier.follows.first.instructeur }
|
||||
let!(:instructeur_2) { create(:instructeur, groupe_instructeurs: [gi_p1]) }
|
||||
|
||||
# one other procedure, dossier followed by a third instructeur
|
||||
# another procedure, dossier followed by a third instructeur
|
||||
let!(:dossier_on_procedure_2) { create(:dossier, :followed, state: Dossier.states.fetch(:en_construction)) }
|
||||
let!(:instructeur_on_procedure_2) { dossier_on_procedure_2.follows.first.instructeur }
|
||||
let(:gi_p2) { dossier.groupe_instructeur }
|
||||
|
|
|
@ -16,7 +16,7 @@ describe ProcedurePresentation do
|
|||
|
||||
context 'for a published procedure' do
|
||||
let(:procedure) { create(:procedure, :published) }
|
||||
let!(:tdc) { { type_champ: :number, libelle: 'libelle 1' } }
|
||||
let(:tdc) { { type_champ: :number, libelle: 'libelle 1' } }
|
||||
|
||||
before do
|
||||
procedure.draft_revision.add_type_de_champ(tdc)
|
||||
|
@ -26,7 +26,7 @@ describe ProcedurePresentation do
|
|||
it { is_expected.to match(['libelle 1']) }
|
||||
|
||||
context 'when there is another published revision with an added tdc' do
|
||||
let!(:added_tdc) { { type_champ: :number, libelle: 'libelle 2' } }
|
||||
let(:added_tdc) { { type_champ: :number, libelle: 'libelle 2' } }
|
||||
|
||||
before do
|
||||
procedure.draft_revision.add_type_de_champ(added_tdc)
|
||||
|
@ -37,7 +37,7 @@ describe ProcedurePresentation do
|
|||
end
|
||||
|
||||
context 'add one tdc above the first one' do
|
||||
let!(:tdc2) { { type_champ: :number, libelle: 'libelle 2' } }
|
||||
let(:tdc2) { { type_champ: :number, libelle: 'libelle 2' } }
|
||||
|
||||
before do
|
||||
created_tdc2 = procedure.draft_revision.add_type_de_champ(tdc2)
|
||||
|
@ -47,7 +47,7 @@ describe ProcedurePresentation do
|
|||
|
||||
it { is_expected.to match(['libelle 2', 'libelle 1']) }
|
||||
|
||||
context 'and finaly, when this tdc is removed' do
|
||||
context 'and finally, when this tdc is removed' do
|
||||
let!(:previous_tdc2) { procedure.published_revision.types_de_champ.find_by(libelle: 'libelle 2') }
|
||||
|
||||
before do
|
||||
|
|
|
@ -320,8 +320,8 @@ describe ProcedurePresentation do
|
|||
|
||||
let(:procedure) { create(:procedure, :for_individual) }
|
||||
|
||||
let!(:first_dossier) { create(:dossier, procedure: procedure, individual: create(:individual, gender: 'M', prenom: 'Alain', nom: 'Antonelli')) }
|
||||
let!(:last_dossier) { create(:dossier, procedure: procedure, individual: create(:individual, gender: 'Mme', prenom: 'Zora', nom: 'Zemmour')) }
|
||||
let!(:first_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'M', prenom: 'Alain', nom: 'Antonelli')) }
|
||||
let!(:last_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'Mme', prenom: 'Zora', nom: 'Zemmour')) }
|
||||
|
||||
context 'for gender column' do
|
||||
let(:column) { 'gender' }
|
||||
|
@ -617,8 +617,8 @@ describe ProcedurePresentation do
|
|||
|
||||
context 'for individual table' do
|
||||
let(:procedure) { create(:procedure, :for_individual) }
|
||||
let!(:kept_dossier) { create(:dossier, procedure: procedure, individual: create(:individual, gender: 'Mme', prenom: 'Josephine', nom: 'Baker')) }
|
||||
let!(:discarded_dossier) { create(:dossier, procedure: procedure, individual: create(:individual, gender: 'M', prenom: 'Jean', nom: 'Tremblay')) }
|
||||
let!(:kept_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'Mme', prenom: 'Josephine', nom: 'Baker')) }
|
||||
let!(:discarded_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'M', prenom: 'Jean', nom: 'Tremblay')) }
|
||||
|
||||
context 'for gender column' do
|
||||
let(:filter) { [{ 'table' => 'individual', 'column' => 'gender', 'value' => 'Mme' }] }
|
||||
|
@ -646,7 +646,7 @@ describe ProcedurePresentation do
|
|||
]
|
||||
end
|
||||
|
||||
let!(:other_kept_dossier) { create(:dossier, procedure: procedure, individual: create(:individual, gender: 'M', prenom: 'Romuald', nom: 'Pistis')) }
|
||||
let!(:other_kept_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'M', prenom: 'Romuald', nom: 'Pistis')) }
|
||||
|
||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
||||
|
|
|
@ -21,11 +21,11 @@ describe DossierSerializer do
|
|||
let(:dossier) { create(:dossier, :en_construction, procedure: create(:procedure, :published, :with_type_de_champ)) }
|
||||
|
||||
before do
|
||||
dossier.champs << build(:champ_carte)
|
||||
dossier.champs << build(:champ_siret)
|
||||
dossier.champs << build(:champ_integer_number)
|
||||
dossier.champs << build(:champ_decimal_number)
|
||||
dossier.champs << build(:champ_linked_drop_down_list)
|
||||
dossier.champs << build(:champ_carte, dossier: dossier)
|
||||
dossier.champs << build(:champ_siret, dossier: dossier)
|
||||
dossier.champs << build(:champ_integer_number, dossier: dossier)
|
||||
dossier.champs << build(:champ_decimal_number, dossier: dossier)
|
||||
dossier.champs << build(:champ_linked_drop_down_list, dossier: dossier)
|
||||
end
|
||||
|
||||
it {
|
||||
|
|
|
@ -92,7 +92,7 @@ describe DossierProjectionService do
|
|||
context 'for individual table' do
|
||||
let(:table) { 'individual' }
|
||||
let(:procedure) { create(:procedure, :for_individual, :with_type_de_champ, :with_type_de_champ_private) }
|
||||
let(:dossier) { create(:dossier, procedure: procedure, individual: create(:individual, nom: 'Martin', prenom: 'Jacques', gender: 'M.')) }
|
||||
let(:dossier) { create(:dossier, procedure: procedure, individual: build(:individual, nom: 'Martin', prenom: 'Jacques', gender: 'M.')) }
|
||||
|
||||
context 'for prenom column' do
|
||||
let(:column) { 'prenom' }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
describe 'wcag rules for usager', js: true do
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ, :with_all_champs, :with_service, :for_individual, :published) }
|
||||
let(:procedure) { create(:procedure, :published, :with_all_champs, :with_service, :for_individual) }
|
||||
let(:password) { 'a very complicated password' }
|
||||
let(:litteraire_user) { create(:user, password: password) }
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ describe 'shared/dossiers/demande.html.haml', type: :view do
|
|||
end
|
||||
|
||||
context 'when dossier was created by an individual' do
|
||||
let(:individual) { create(:individual) }
|
||||
let(:individual) { build(:individual) }
|
||||
|
||||
it 'renders the individual identity infos' do
|
||||
expect(subject).to include(individual.gender)
|
||||
|
|
Loading…
Reference in a new issue