feat(demarche): create and prefill a dossier with POST request (#8233)

* add base controller for public api

* add dossiers controller with basic checks

* create the dossier

* ensure content-type is json

* prefill dossier with given values

* mark a dossier as prefilled

When a dossier is prefilled, it's allowed not to have a user.

Plus, we add a secure token to the dossier, which we will need later to set a
user after sign in / sign up.

* set user as owner of an orphan prefilled dossier

When a visitor comes from the dossier_url answered by the public api,
the dossier is orphan:
- when the user is already authenticated: they become the owner
- when the user is not authenticated: they can sign in / sign up / france_connect
and then they become the owner

So here is the procedure:
- allow to sign in / sign up / france connect when user is unauthenticated
- set dossier ownership when the dossier is orphan
- check dossier ownership when the dossier is not
- redirect to brouillon path when user is signed in and owner

* mark the dossier as prefilled when it's prefilled
(even with a GET request, because it will be useful later on, for
exmample in order to cleanup the unused prefilled dossiers)

* system spec: prefilling dossier with post request
This commit is contained in:
Sébastien Carceles 2023-01-03 14:46:10 +01:00 committed by GitHub
parent 3f4e7ab1f5
commit 20136b7ac8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 760 additions and 111 deletions

View file

@ -0,0 +1,29 @@
class API::Public::V1::BaseController < APIController
skip_forgery_protection
before_action :check_content_type_is_json
protected
def render_missing_param(param_name)
render_error("#{param_name} is missing", :bad_request)
end
def render_bad_request(error_message)
render_error(error_message, :bad_request)
end
def render_not_found(resource_name, resource_id)
render_error("#{resource_name} #{resource_id} is not found", :not_found)
end
private
def check_content_type_is_json
render_error("Content-Type should be json", :bad_request) unless request.headers['Content-Type'] == 'application/json'
end
def render_error(message, status)
render json: { error: message }, status: status
end
end

View file

@ -0,0 +1,26 @@
class API::Public::V1::DossiersController < API::Public::V1::BaseController
before_action :retrieve_procedure
def create
dossier = Dossier.new(
revision: @procedure.active_revision,
groupe_instructeur: @procedure.defaut_groupe_instructeur_for_new_dossier,
state: Dossier.states.fetch(:brouillon),
prefilled: true
)
dossier.build_default_individual
if dossier.save
dossier.prefill!(PrefillParams.new(dossier, params.to_unsafe_h).to_a)
render json: { dossier_url: commencer_url(@procedure.path, prefill_token: dossier.prefill_token) }, status: :created
else
render_bad_request(dossier.errors.full_messages.to_sentence)
end
end
private
def retrieve_procedure
@procedure = Procedure.publiees_ou_brouillons.find_by(id: params[:id])
render_not_found("procedure", params[:id]) if @procedure.blank?
end
end

View file

@ -5,12 +5,14 @@ module ProcedureContextConcern
include Devise::StoreLocationExtension
def restore_procedure_context
if has_stored_procedure_path?
@procedure = find_procedure_in_context
return unless has_stored_procedure_path?
if @procedure.blank?
invalid_procedure_context
end
@procedure = find_procedure_in_context
if @procedure.blank?
invalid_procedure_context
else
@prefill_token = find_prefill_token_in_context
end
end
@ -33,6 +35,11 @@ module ProcedureContextConcern
end
end
def find_prefill_token_in_context
uri = URI(get_stored_location_for(:user))
CGI.parse(uri.query).dig("prefill_token")&.first if uri.query
end
def invalid_procedure_context
clear_stored_location_for(:user)
flash.alert = t('errors.messages.procedure_not_found')

View file

@ -4,6 +4,10 @@ module Users
layout 'procedure_context'
before_action :retrieve_prefilled_dossier, if: -> { params[:prefill_token].present? }, only: :commencer
before_action :set_prefilled_dossier_ownership, if: -> { user_signed_in? && @prefilled_dossier&.orphan? }, only: :commencer
before_action :check_prefilled_dossier_ownership, if: -> { user_signed_in? && @prefilled_dossier }, only: :commencer
def commencer
@procedure = retrieve_procedure
return procedure_not_found if @procedure.blank? || @procedure.brouillon?
@ -74,6 +78,21 @@ module Users
Procedure.publiees.or(Procedure.brouillons).or(Procedure.closes).find_by(path: params[:path])
end
def retrieve_prefilled_dossier
@prefilled_dossier = Dossier.state_brouillon.prefilled.find_by!(prefill_token: params[:prefill_token])
end
# The prefilled dossier is not owned yet, and the user is signed in: they become the new owner
def set_prefilled_dossier_ownership
@prefilled_dossier.update!(user: current_user)
DossierMailer.with(dossier: @prefilled_dossier).notify_new_draft.deliver_later
end
# The prefilled dossier is owned by another user: raise an exception
def check_prefilled_dossier_ownership
raise ActiveRecord::RecordNotFound unless @prefilled_dossier.owned_by?(current_user)
end
def procedure_not_found
procedure = Procedure.find_by(path: params[:path])
@ -92,7 +111,7 @@ module Users
end
def store_user_location!(procedure)
store_location_for(:user, helpers.procedure_lien(procedure))
store_location_for(:user, helpers.procedure_lien(procedure, prefill_token: params[:prefill_token]))
end
def generate_empty_pdf(revision)

View file

@ -45,7 +45,7 @@ class Users::ConfirmationsController < Devise::ConfirmationsController
end
if procedure_from_params
commencer_path(path: procedure_from_params.path)
commencer_path(path: procedure_from_params.path, prefill_token: params[:prefill_token])
elsif signed_in?
# Will try to use `stored_location_for` to find a path
after_sign_in_path_for(resource_name)

View file

@ -26,6 +26,7 @@ class Users::RegistrationsController < Devise::RegistrationsController
# all Devise code.
# So instead we use a per-request global variable.
CurrentConfirmation.procedure_after_confirmation = @procedure
CurrentConfirmation.prefill_token = @prefill_token
# Handle existing user trying to sign up again
existing_user = User.find_by(email: params[:user][:email])

View file

@ -1,9 +1,9 @@
module ProcedureHelper
def procedure_lien(procedure)
def procedure_lien(procedure, prefill_token: nil)
if procedure.brouillon?
commencer_test_url(path: procedure.path)
commencer_test_url(path: procedure.path, prefill_token: prefill_token)
else
commencer_url(path: procedure.path)
commencer_url(path: procedure.path, prefill_token: prefill_token)
end
end

View file

@ -23,6 +23,7 @@ class DeviseUserMailer < Devise::Mailer
def confirmation_instructions(record, token, opts = {})
opts[:from] = NO_REPLY_EMAIL
@procedure = opts[:procedure_after_confirmation] || nil
@prefill_token = opts[:prefill_token]
super
end
end

View file

@ -3,10 +3,11 @@
module DossierPrefillableConcern
extend ActiveSupport::Concern
def prefill!(champs_attributes)
return if champs_attributes.empty?
def prefill!(champs_public_attributes)
attr = { prefilled: true }
attr[:champs_public_attributes] = champs_public_attributes.map { |h| h.merge(prefilled: true) } if champs_public_attributes.any?
assign_attributes(champs_attributes: champs_attributes.map { |h| h.merge(prefilled: true) })
assign_attributes(attr)
save(validate: false)
end
end

View file

@ -1,3 +1,4 @@
class CurrentConfirmation < ActiveSupport::CurrentAttributes
attribute :procedure_after_confirmation
attribute :prefill_token
end

View file

@ -28,6 +28,8 @@
# last_champ_updated_at :datetime
# last_commentaire_updated_at :datetime
# motivation :text
# prefill_token :string
# prefilled :boolean
# private_search_terms :text
# processed_at :datetime
# search_terms :text
@ -70,6 +72,8 @@ class Dossier < ApplicationRecord
DAYS_AFTER_EXPIRATION = 5
INTERVAL_EXPIRATION = "#{MONTHS_AFTER_EXPIRATION} month #{DAYS_AFTER_EXPIRATION} days"
has_secure_token :prefill_token
has_one :etablissement, dependent: :destroy
has_one :individual, validate: false, dependent: :destroy
has_one :attestation, dependent: :destroy
@ -218,11 +222,12 @@ class Dossier < ApplicationRecord
scope :state_termine, -> { where(state: TERMINE) }
scope :state_not_termine, -> { where.not(state: TERMINE) }
scope :archived, -> { where(archived: true) }
scope :not_archived, -> { where(archived: false) }
scope :hidden_by_user, -> { where.not(hidden_by_user_at: nil) }
scope :hidden_by_administration, -> { where.not(hidden_by_administration_at: nil) }
scope :visible_by_user, -> { where(for_procedure_preview: false).or(where(for_procedure_preview: nil)).where(hidden_by_user_at: nil) }
scope :archived, -> { where(archived: true) }
scope :not_archived, -> { where(archived: false) }
scope :prefilled, -> { where(prefilled: true) }
scope :hidden_by_user, -> { where.not(hidden_by_user_at: nil) }
scope :hidden_by_administration, -> { where.not(hidden_by_administration_at: nil) }
scope :visible_by_user, -> { where(for_procedure_preview: false).or(where(for_procedure_preview: nil)).where(hidden_by_user_at: nil) }
scope :visible_by_administration, -> {
state_not_brouillon
.where(hidden_by_administration_at: nil)
@ -435,7 +440,7 @@ class Dossier < ApplicationRecord
after_save :send_web_hook
validates :user, presence: true, if: -> { deleted_user_email_never_send.nil? }
validates :user, presence: true, if: -> { deleted_user_email_never_send.nil? }, unless: -> { prefilled }
validates :individual, presence: true, if: -> { revision.procedure.for_individual? }
validates :groupe_instructeur, presence: true, if: -> { !brouillon? }
@ -718,6 +723,17 @@ class Dossier < ApplicationRecord
end
end
def orphan?
prefilled? && user.nil?
end
def owned_by?(a_user)
return false if a_user.nil?
return false if orphan?
user == a_user
end
def log_operations?
!procedure.brouillon? && !brouillon?
end

View file

@ -206,7 +206,7 @@ class Procedure < ApplicationRecord
scope :brouillons, -> { where(aasm_state: :brouillon) }
scope :publiees, -> { where(aasm_state: :publiee) }
scope :publiees_ou_brouillons, -> { publiees.or(brouillons) }
scope :publiees_ou_brouillons, -> { where(aasm_state: [:publiee, :brouillon]) }
scope :closes, -> { where(aasm_state: [:close, :depubliee]) }
scope :opendata, -> { where(opendata: true) }
scope :publiees_ou_closes, -> { where(aasm_state: [:publiee, :close, :depubliee]) }

View file

@ -80,6 +80,7 @@ class User < ApplicationRecord
# Make our procedure_after_confirmation available to the Mailer
opts[:procedure_after_confirmation] = CurrentConfirmation.procedure_after_confirmation
opts[:prefill_token] = CurrentConfirmation.prefill_token
send_devise_notification(:confirmation_instructions, @raw_confirmation_token, opts)
end

View file

@ -3,12 +3,12 @@
.commencer.form
- if !user_signed_in?
%h2.huge-title= t('views.commencer.show.start_procedure')
= render partial: 'shared/france_connect_login', locals: { url: commencer_france_connect_path(path: @procedure.path) }
= link_to commencer_sign_up_path(path: @procedure.path), class: 'fr-btn fr-btn--lg fr-my-2w' do
= render partial: 'shared/france_connect_login', locals: { url: commencer_france_connect_path(path: @procedure.path, prefill_token: @prefilled_dossier&.prefill_token) }
= link_to commencer_sign_up_path(path: @procedure.path, prefill_token: @prefilled_dossier&.prefill_token), class: 'fr-btn fr-btn--lg fr-my-2w' do
= t('views.shared.account.create')
%span.optional-on-small-screens.fr-ml-1v
#{APPLICATION_NAME}
= link_to t('views.shared.account.already_user'), commencer_sign_in_path(path: @procedure.path), class: 'fr-btn fr-btn--secondary fr-btn--lg fr-my-2w'
= link_to t('views.shared.account.already_user'), commencer_sign_in_path(path: @procedure.path, prefill_token: @prefilled_dossier&.prefill_token), class: 'fr-btn fr-btn--secondary fr-btn--lg fr-my-2w'
- else
- revision = @revision.draft? ? @revision : @procedure.revisions.where.not(id: @procedure.draft_revision_id)
@ -19,6 +19,13 @@
- if dossiers.empty?
= link_to t('views.commencer.show.start_procedure'), url_for_new_dossier(@revision), class: 'fr-btn fr-btn--lg fr-my-2w'
- elsif @prefilled_dossier
%h2.huge-title= t('views.commencer.show.prefilled_draft')
%p
= t('views.commencer.show.prefilled_draft_detail_html', time_ago: time_ago_in_words(@prefilled_dossier.created_at), procedure: @prefilled_dossier.procedure.libelle)
= link_to t('views.commencer.show.continue_file'), brouillon_dossier_path(@prefilled_dossier), class: 'fr-btn fr-btn--lg fr-my-2w'
= link_to t('views.commencer.show.start_new_file'), url_for_new_dossier(@revision), class: 'fr-btn fr-btn--lg fr-btn--secondary fr-my-2w'
- elsif drafts.size == 1 && not_drafts.empty?
- dossier = drafts.first
%h2.huge-title= t('views.commencer.show.already_draft')

View file

@ -6,7 +6,7 @@
%p
Pour activer votre compte sur #{APPLICATION_NAME}, veuillez cliquer sur le lien suivant :
- link = confirmation_url(@user, confirmation_token: @token, procedure_id: @procedure&.id)
- link = confirmation_url(@user, confirmation_token: @token, procedure_id: @procedure&.id, prefill_token: @prefill_token)
= link_to(link, link)
- else

View file

@ -92,6 +92,8 @@ en:
start_procedure: Start the procedure
existing_dossiers: You already have files for this procedure
show_dossiers: View my current files
prefilled_draft: "You have a prefilled file"
prefilled_draft_detail_html: "You prefilled a file for the \"%{procedure}\" procedure <strong>%{time_ago} ago</strong>"
already_draft: "You already started to fill a file"
already_draft_detail_html: "You started to fill a file for the \"%{procedure}\" procedure <strong>%{time_ago} ago</strong>"
already_not_draft: "You already submitted a file"

View file

@ -82,6 +82,8 @@ fr:
start_procedure: Commencer la démarche
existing_dossiers: Vous avez déjà des dossiers pour cette démarche
show_dossiers: Voir mes dossiers en cours
prefilled_draft: "Vous avez un dossier prérempli"
prefilled_draft_detail_html: "Il y a <strong>%{time_ago}</strong>, vous avez prérempli un dossier sur la démarche « %{procedure} »."
already_draft: "Vous avez déjà commencé à remplir un dossier"
already_draft_detail_html: "Il y a <strong>%{time_ago}</strong>, vous avez commencé à remplir un dossier sur la démarche « %{procedure} »."
already_not_draft: "Vous avez déjà déposé un dossier"

View file

@ -263,7 +263,11 @@ Rails.application.routes.draw do
namespace :public do
namespace :v1 do
resources :dossiers, only: :create
resources :demarches, only: [] do
member do
resources :dossiers, only: :create
end
end
end
end
end

View file

@ -0,0 +1,6 @@
class AddPrefillFieldsToDossiers < ActiveRecord::Migration[6.1]
def change
add_column :dossiers, :prefill_token, :string
add_column :dossiers, :prefilled, :boolean
end
end

View file

@ -0,0 +1,7 @@
class AddPrefillTokenIndexToDossiers < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
add_index :dossiers, :prefill_token, unique: true, algorithm: :concurrently
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_12_05_144624) do
ActiveRecord::Schema.define(version: 2022_12_13_084442) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
@ -298,13 +298,13 @@ ActiveRecord::Schema.define(version: 2022_12_05_144624) do
t.boolean "automatic_operation", default: false, null: false
t.bigint "bill_signature_id"
t.datetime "created_at", null: false
t.jsonb "data"
t.text "digest"
t.bigint "dossier_id"
t.datetime "executed_at"
t.datetime "keep_until"
t.string "operation", null: false
t.datetime "updated_at", null: false
t.jsonb "data"
t.index ["bill_signature_id"], name: "index_dossier_operation_logs_on_bill_signature_id"
t.index ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id"
t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until"
@ -363,6 +363,8 @@ ActiveRecord::Schema.define(version: 2022_12_05_144624) do
t.datetime "last_commentaire_updated_at"
t.text "motivation"
t.bigint "parent_dossier_id"
t.string "prefill_token"
t.boolean "prefilled"
t.string "private_search_terms"
t.datetime "processed_at"
t.bigint "revision_id"
@ -376,6 +378,7 @@ ActiveRecord::Schema.define(version: 2022_12_05_144624) do
t.index ["dossier_transfer_id"], name: "index_dossiers_on_dossier_transfer_id"
t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id"
t.index ["hidden_at"], name: "index_dossiers_on_hidden_at"
t.index ["prefill_token"], name: "index_dossiers_on_prefill_token", unique: true
t.index ["revision_id"], name: "index_dossiers_on_revision_id"
t.index ["state"], name: "index_dossiers_on_state"
t.index ["user_id"], name: "index_dossiers_on_user_id"
@ -807,6 +810,7 @@ ActiveRecord::Schema.define(version: 2022_12_05_144624) do
t.datetime "reset_password_sent_at"
t.string "reset_password_token"
t.integer "sign_in_count", default: 0, null: false
t.boolean "team_account", default: false
t.string "unlock_token"
t.datetime "updated_at"
t.index ["email"], name: "index_super_admins_on_email", unique: true
@ -885,7 +889,6 @@ ActiveRecord::Schema.define(version: 2022_12_05_144624) do
t.string "reset_password_token"
t.integer "sign_in_count", default: 0, null: false
t.string "siret"
t.boolean "team_account", default: false
t.text "unconfirmed_email"
t.string "unlock_token"
t.datetime "updated_at"

View file

@ -0,0 +1,135 @@
RSpec.describe API::Public::V1::DossiersController, type: :controller do
include Rails.application.routes.url_helpers
describe '#create' do
# Request prototype:
# curl --request POST 'http://localhost:3000/api/public/v1/demarches/2/dossiers' \
# --header 'Content-Type: application/json' \
# --data '{"champ_Q2hhbXAtMjI=": "personne@fournisseur.fr"}'
context 'when the request content type is json' do
let(:params) { { id: procedure.id } }
subject(:create_request) do
request.headers["Content-Type"] = "application/json"
post :create, params: params
end
shared_examples 'the procedure is found' do
context 'when the dossier can be saved' do
it { expect(create_request).to have_http_status(:created) }
it { expect { create_request }.to change { Dossier.count }.by(1) }
it "marks the created dossier as prefilled" do
create_request
expect(Dossier.last.prefilled).to eq(true)
end
it "creates the dossier without a user" do
create_request
expect(Dossier.last.user).to eq(nil)
end
it "responds with the brouillon dossier path" do
create_request
expect(JSON.parse(response.body)["dossier_url"]).to eq(
"http://test.host#{commencer_path(procedure.path, prefill_token: Dossier.last.prefill_token)}"
)
end
context 'when prefill values are given' do
let!(:type_de_champ_1) { create(:type_de_champ_text, procedure: procedure) }
let(:value_1) { "any value" }
let!(:type_de_champ_2) { create(:type_de_champ_textarea, procedure: procedure) }
let(:value_2) { "another value" }
let(:params) {
{
id: procedure.id,
"champ_#{type_de_champ_1.to_typed_id}" => value_1,
"champ_#{type_de_champ_2.to_typed_id}" => value_2
}
}
it "prefills the dossier's champs with the given values" do
create_request
dossier = Dossier.last
expect(find_champ_by_stable_id(dossier, type_de_champ_1.stable_id).value).to eq(value_1)
expect(find_champ_by_stable_id(dossier, type_de_champ_2.stable_id).value).to eq(value_2)
end
end
end
context 'when the dossier can not be saved' do
before do
allow_any_instance_of(Dossier).to receive(:save).and_return(false)
allow_any_instance_of(Dossier).to receive(:errors).and_return(
ActiveModel::Errors.new(Dossier.new).tap { |e| e.add(:base, "something went wrong") }
)
create_request
end
it { expect(response).to have_http_status(:bad_request) }
it { expect(response).to have_failed_with("something went wrong") }
end
end
shared_examples 'the procedure is not found' do
before { create_request }
it { expect(response).to have_http_status(:not_found) }
it { expect(response).to have_failed_with("procedure #{procedure.id} is not found") }
end
context 'when the procedure is found' do
context 'when the procedure is publiee' do
it_behaves_like 'the procedure is found' do
let(:procedure) { create(:procedure, :published) }
end
end
context 'when the procedure is brouillon' do
it_behaves_like 'the procedure is found' do
let(:procedure) { create(:procedure, :draft) }
end
end
context 'when the procedure is not publiee and not brouillon' do
it_behaves_like 'the procedure is not found' do
let(:procedure) { create(:procedure, :closed) }
end
end
end
context 'when the procedure is not found' do
it_behaves_like 'the procedure is not found' do
let(:procedure) { double(Procedure, id: -1) }
end
end
end
context 'when the request content type is not json' do
subject(:create_request) do
request.headers["Content-Type"] = "application/xhtml+xml"
post :create, params: { id: 0 }
end
before { create_request }
it { expect(response).to have_http_status(:bad_request) }
it { expect(response).to have_failed_with("Content-Type should be json") }
end
end
private
def find_champ_by_stable_id(dossier, stable_id)
dossier.champs_public.joins(:type_de_champ).find_by(types_de_champ: { stable_id: stable_id })
end
end

View file

@ -69,6 +69,19 @@ RSpec.describe ProcedureContextConcern, type: :controller do
expect(subject.status).to eq 200
expect(assigns(:procedure)).to eq test_procedure
end
context 'when a prefill token has been stored' do
let(:dossier) { create :dossier, :prefilled, procedure: test_procedure }
before do
controller.store_location_for(:user, commencer_test_path(path: test_procedure.path, prefill_token: dossier.prefill_token))
end
it 'succeeds, and assigns the prefill token on the controller' do
expect(subject.status).to eq 200
expect(assigns(:prefill_token)).to eq dossier.prefill_token
end
end
end
context 'when the stored procedure is published' do
@ -82,6 +95,19 @@ RSpec.describe ProcedureContextConcern, type: :controller do
expect(subject.status).to eq 200
expect(assigns(:procedure)).to eq published_procedure
end
context 'when a prefill token has been stored' do
let(:dossier) { create :dossier, :prefilled, procedure: published_procedure }
before do
controller.store_location_for(:user, commencer_path(path: published_procedure.path, prefill_token: dossier.prefill_token))
end
it 'succeeds, and assigns the prefill token on the controller' do
expect(subject.status).to eq 200
expect(assigns(:prefill_token)).to eq dossier.prefill_token
end
end
end
end
end

View file

@ -72,6 +72,85 @@ describe Users::CommencerController, type: :controller do
expect(subject).to redirect_to(commencer_path(path: replaced_by_procedure.path))
end
end
context 'when a dossier has been prefilled' do
let(:dossier) { create(:dossier, :brouillon, :prefilled, user: user) }
let(:path) { dossier.procedure.path }
subject { get :commencer, params: { path: path, prefill_token: dossier.prefill_token } }
shared_examples 'a prefilled brouillon dossier retriever' do
context 'when the dossier is a prefilled brouillon and the prefill token is present' do
it 'retrieves the dossier' do
subject
expect(assigns(:prefilled_dossier)).to eq(dossier)
end
end
context 'when the dossier is not prefilled' do
before do
dossier.prefilled = false
dossier.save(validate: false)
end
it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
end
context 'when the dossier is not a brouillon' do
before { dossier.en_construction! }
it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
end
context 'when the prefill token does not match any dossier' do
before { dossier.prefill_token = "totoro" }
it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
end
end
context 'when the user is unauthenticated' do
let(:user) { nil }
it_behaves_like 'a prefilled brouillon dossier retriever'
end
context 'when the user is authenticated' do
context 'when the dossier already has an owner' do
let(:user) { create(:user) }
context 'when the user is the dossier owner' do
before { sign_in user }
it_behaves_like 'a prefilled brouillon dossier retriever'
end
context 'when the user is not the dossier owner' do
before { sign_in create(:user) }
it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
end
end
context 'when the dossier does not have an owner yet' do
let(:user) { nil }
let(:newly_authenticated_user) { create(:user) }
before { sign_in newly_authenticated_user }
it { expect { subject }.to change { dossier.reload.user }.from(nil).to(newly_authenticated_user) }
it 'sends the notify_new_draft email' do
expect { perform_enqueued_jobs { subject } }.to change { ActionMailer::Base.deliveries.count }.by(1)
dossier = Dossier.last
mail = ActionMailer::Base.deliveries.last
expect(mail.subject).to eq("Retrouvez votre brouillon pour la démarche « #{dossier.procedure.libelle} »")
expect(mail.html_part.body).to include(dossier_path(dossier))
end
end
end
end
end
describe '#commencer_test' do
@ -105,6 +184,13 @@ describe Users::CommencerController, type: :controller do
end
end
shared_examples 'a prefill token storage' do
it 'stores the prefill token' do
subject
expect(controller.stored_location_for(:user)).to include('prefill_token')
end
end
describe '#sign_in' do
context 'for a published procedure' do
subject { get :sign_in, params: { path: published_procedure.path } }
@ -115,6 +201,12 @@ describe Users::CommencerController, type: :controller do
end
it { expect(subject).to redirect_to(new_user_session_path) }
context 'when a prefill token is given' do
subject { get :sign_in, params: { path: published_procedure.path, prefill_token: 'prefill_token' } }
it_behaves_like 'a prefill token storage'
end
end
context 'for a draft procedure' do
@ -126,6 +218,12 @@ describe Users::CommencerController, type: :controller do
end
it { expect(subject).to redirect_to(new_user_session_path) }
context 'when a prefill token is given' do
subject { get :sign_in, params: { path: draft_procedure.path, prefill_token: 'prefill_token' } }
it_behaves_like 'a prefill token storage'
end
end
context 'when the path doesnt exist' do
@ -147,6 +245,12 @@ describe Users::CommencerController, type: :controller do
end
it { expect(subject).to redirect_to(new_user_registration_path) }
context 'when a prefill token is given' do
subject { get :sign_up, params: { path: published_procedure.path, prefill_token: 'prefill_token' } }
it_behaves_like 'a prefill token storage'
end
end
context 'for a draft procedure' do
@ -158,6 +262,12 @@ describe Users::CommencerController, type: :controller do
end
it { expect(subject).to redirect_to(new_user_registration_path) }
context 'when a prefill token is given' do
subject { get :sign_up, params: { path: draft_procedure.path, prefill_token: 'prefill_token' } }
it_behaves_like 'a prefill token storage'
end
end
context 'when the path doesnt exist' do
@ -179,6 +289,12 @@ describe Users::CommencerController, type: :controller do
end
it { expect(subject).to redirect_to(france_connect_particulier_path) }
context 'when a prefill token is given' do
subject { get :france_connect, params: { path: published_procedure.path, prefill_token: 'prefill_token' } }
it_behaves_like 'a prefill token storage'
end
end
context 'for a draft procedure' do
@ -190,6 +306,12 @@ describe Users::CommencerController, type: :controller do
end
it { expect(subject).to redirect_to(france_connect_particulier_path) }
context 'when a prefill token is given' do
subject { get :france_connect, params: { path: draft_procedure.path, prefill_token: 'prefill_token' } }
it_behaves_like 'a prefill token storage'
end
end
context 'when the path doesnt exist' do

View file

@ -256,5 +256,9 @@ FactoryBot.define do
dossier.save!
end
end
trait :prefilled do
prefilled { true }
end
end
end

View file

@ -8,6 +8,10 @@ class DeviseUserMailerPreview < ActionMailer::Preview
DeviseUserMailer.confirmation_instructions(user, "faketoken", {})
end
def confirmation_instructions___with_procedure_and_prefill_token
DeviseUserMailer.confirmation_instructions(user, "faketoken", procedure_after_confirmation: procedure, prefill_token: "prefill_token")
end
def reset_password_instructions
DeviseUserMailer.reset_password_instructions(user, "faketoken", {})
end

View file

@ -8,12 +8,19 @@ RSpec.describe DossierPrefillableConcern do
subject(:fill) { dossier.prefill!(values); dossier.reload }
shared_examples 'a dossier marked as prefilled' do
it 'marks the dossier as prefilled' do
expect { fill }.to change { dossier.reload.prefilled }.from(nil).to(true)
end
end
context 'when champs_public_attributes is empty' do
let(:values) { [] }
it "does nothing" do
expect(dossier).not_to receive(:save)
fill
it_behaves_like 'a dossier marked as prefilled'
it "doesn't change champs_public" do
expect { fill }.not_to change { dossier.champs_public.to_a }
end
end
@ -30,6 +37,8 @@ RSpec.describe DossierPrefillableConcern do
let(:values) { [{ id: champ_id_1, value: value_1 }, { id: champ_id_2, value: value_2 }] }
it_behaves_like 'a dossier marked as prefilled'
it "updates the champs with the new values and mark them as prefilled" do
fill
@ -48,6 +57,8 @@ RSpec.describe DossierPrefillableConcern do
let(:values) { [{ id: champ_id, value: value }] }
it_behaves_like 'a dossier marked as prefilled'
it "still updates the champ" do
expect { fill }.to change { dossier.champs_public.first.value }.from(nil).to(value)
end

View file

@ -25,6 +25,20 @@ describe Dossier do
subject(:dossier) { create(:dossier, procedure: procedure) }
it { is_expected.to validate_presence_of(:individual) }
it { is_expected.to validate_presence_of(:user) }
context 'when dossier has deleted_user_email_never_send' do
subject(:dossier) { create(:dossier, procedure: procedure, deleted_user_email_never_send: "seb@totoro.org") }
it { is_expected.not_to validate_presence_of(:user) }
end
context 'when dossier is prefilled' do
subject(:dossier) { create(:dossier, procedure: procedure, prefilled: true) }
it { is_expected.not_to validate_presence_of(:user) }
end
end
describe 'with_champs' do
@ -1928,6 +1942,70 @@ describe Dossier do
it { is_expected.to belong_to(:batch_operation).optional }
end
describe '#orphan?' do
subject(:orphan) { dossier.orphan? }
context 'when the dossier is prefilled' do
context 'when the dossier has a user' do
let(:dossier) { build(:dossier, :prefilled) }
it { expect(orphan).to be_falsey }
end
context 'when the dossier does not have a user' do
let(:dossier) { build(:dossier, :prefilled, user: nil) }
it { expect(orphan).to be_truthy }
end
end
context 'when the dossier is not prefilled' do
context 'when the dossier has a user' do
let(:dossier) { build(:dossier) }
it { expect(orphan).to be_falsey }
end
context 'when the dossier does not have a user' do
let(:dossier) { build(:dossier, user: nil) }
it { expect(orphan).to be_falsey }
end
end
end
describe '#owned_by?' do
subject(:owned_by) { dossier.owned_by?(user) }
context 'when the dossier is orphan' do
let(:dossier) { build(:dossier, user: nil) }
let(:user) { build(:user) }
it { expect(owned_by).to be_falsey }
end
context 'when the given user is nil' do
let(:dossier) { build(:dossier) }
let(:user) { nil }
it { expect(owned_by).to be_falsey }
end
context 'when the dossier has a user and it is not the given user' do
let(:dossier) { build(:dossier) }
let(:user) { build(:user) }
it { expect(owned_by).to be_falsey }
end
context 'when the dossier has a user and it is the given user' do
let(:dossier) { build(:dossier, user: user) }
let(:user) { build(:user) }
it { expect(owned_by).to be_truthy }
end
end
private
def count_for_month(processed_by_month, month)

View file

@ -0,0 +1,5 @@
RSpec::Matchers.define :have_failed_with do |expected|
match do |response|
JSON.parse(response.body).with_indifferent_access.dig(:error) == expected
end
end

View file

@ -0,0 +1,22 @@
shared_examples "the user has got a prefilled dossier, owned by themselves" do
scenario "the user has got a prefilled dossier, owned by themselves" do
siret = '41816609600051'
stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{siret}/)
.to_return(status: 200, body: File.read('spec/fixtures/files/api_entreprise/etablissements.json'))
expect(dossier.user).to eq(user)
expect(page).to have_current_path siret_dossier_path(procedure.dossiers.last)
fill_in 'Numéro SIRET', with: siret
click_on 'Valider'
expect(page).to have_current_path(etablissement_dossier_path(dossier))
expect(page).to have_content('OCTO TECHNOLOGY')
click_on 'Continuer avec ces informations'
expect(page).to have_current_path(brouillon_dossier_path(dossier))
expect(page).to have_field(type_de_champ_text.libelle, with: text_value)
expect(page).to have_field(type_de_champ_phone.libelle, with: phone_value)
expect(page).to have_css('.field_with_errors', text: type_de_champ_phone.libelle)
end
end

View file

@ -0,0 +1,87 @@
describe 'Prefilling a dossier (with a GET request):' do
let(:password) { 'my-s3cure-p4ssword' }
let(:procedure) { create(:procedure, :published, opendata: true) }
let(:dossier) { procedure.dossiers.last }
let(:type_de_champ_text) { create(:type_de_champ_text, procedure: procedure) }
let(:type_de_champ_phone) { create(:type_de_champ_phone, procedure: procedure) }
let(:text_value) { "My Neighbor Totoro is the best movie ever" }
let(:phone_value) { "invalid phone value" }
context 'when authenticated' do
it_behaves_like "the user has got a prefilled dossier, owned by themselves" do
let(:user) { create(:user, password: password) }
before do
visit "/users/sign_in"
sign_in_with user.email, password
visit commencer_path(
path: procedure.path,
"champ_#{type_de_champ_text.to_typed_id}" => text_value,
"champ_#{type_de_champ_phone.to_typed_id}" => phone_value
)
click_on "Commencer la démarche"
end
end
end
context 'when unauthenticated' do
before do
visit commencer_path(
path: procedure.path,
"champ_#{type_de_champ_text.to_typed_id}" => text_value,
"champ_#{type_de_champ_phone.to_typed_id}" => phone_value
)
end
context 'when the user signs in with email and password' do
it_behaves_like "the user has got a prefilled dossier, owned by themselves" do
let!(:user) { create(:user, password: password) }
before do
click_on "Jai déjà un compte"
sign_in_with user.email, password
click_on "Commencer la démarche"
end
end
end
context 'when the user signs up with email and password' do
it_behaves_like "the user has got a prefilled dossier, owned by themselves" do
let(:user_email) { generate :user_email }
let(:user) { User.find_by(email: user_email) }
before do
click_on "Créer un compte #{APPLICATION_NAME}"
sign_up_with user_email, password
expect(page).to have_content "nous avons besoin de vérifier votre adresse #{user_email}"
click_confirmation_link_for user_email
expect(page).to have_content('Votre compte a bien été confirmé.')
click_on "Commencer la démarche"
end
end
end
context 'when the user signs up with FranceConnect' do
it_behaves_like "the user has got a prefilled dossier, owned by themselves" do
let(:user) { User.last }
before do
allow_any_instance_of(FranceConnectParticulierClient).to receive(:authorization_uri).and_return(france_connect_particulier_callback_path(code: "c0d3"))
allow(FranceConnectService).to receive(:retrieve_user_informations_particulier).and_return(build(:france_connect_information))
page.find('.fr-connect').click
click_on "Commencer la démarche"
end
end
end
end
end

View file

@ -0,0 +1,102 @@
describe 'Prefilling a dossier (with a POST request):' do
let(:password) { 'my-s3cure-p4ssword' }
let(:procedure) { create(:procedure, :published) }
let(:dossier) { procedure.dossiers.last }
let(:type_de_champ_text) { create(:type_de_champ_text, procedure: procedure) }
let(:type_de_champ_phone) { create(:type_de_champ_phone, procedure: procedure) }
let(:text_value) { "My Neighbor Totoro is the best movie ever" }
let(:phone_value) { "invalid phone value" }
scenario "the user get the URL of a prefilled orphan brouillon dossier" do
dossier_url = create_and_prefill_dossier_with_post_request
expect(dossier_url).to eq(commencer_path(procedure.path, prefill_token: dossier.prefill_token))
end
describe 'visit the dossier URL' do
context 'when authenticated' do
it_behaves_like "the user has got a prefilled dossier, owned by themselves" do
let(:user) { create(:user, password: password) }
before do
visit "/users/sign_in"
sign_in_with user.email, password
visit create_and_prefill_dossier_with_post_request
expect(page).to have_content('Vous avez un dossier prérempli')
click_on 'Continuer à remplir mon dossier'
end
end
end
context 'when unauthenticated' do
before { visit create_and_prefill_dossier_with_post_request }
context 'when the user signs in with email and password' do
it_behaves_like "the user has got a prefilled dossier, owned by themselves" do
let(:user) { create(:user, password: password) }
before do
click_on "Jai déjà un compte"
sign_in_with user.email, password
expect(page).to have_content('Vous avez un dossier prérempli')
click_on 'Continuer à remplir mon dossier'
end
end
end
context 'when the user signs up with email and password' do
it_behaves_like "the user has got a prefilled dossier, owned by themselves" do
let(:user_email) { generate :user_email }
let(:user) { User.find_by(email: user_email) }
before do
click_on "Créer un compte #{APPLICATION_NAME}"
sign_up_with user_email, password
expect(page).to have_content "nous avons besoin de vérifier votre adresse #{user_email}"
click_confirmation_link_for user_email
expect(page).to have_content('Votre compte a bien été confirmé.')
expect(page).to have_content('Vous avez un dossier prérempli')
click_on 'Continuer à remplir mon dossier'
end
end
end
context 'when the user signs up with FranceConnect' do
it_behaves_like "the user has got a prefilled dossier, owned by themselves" do
let(:user) { User.last }
before do
allow_any_instance_of(FranceConnectParticulierClient).to receive(:authorization_uri).and_return(france_connect_particulier_callback_path(code: "c0d3"))
allow(FranceConnectService).to receive(:retrieve_user_informations_particulier).and_return(build(:france_connect_information))
page.find('.fr-connect').click
expect(page).to have_content('Vous avez un dossier prérempli')
click_on 'Continuer à remplir mon dossier'
end
end
end
end
end
private
def create_and_prefill_dossier_with_post_request
session = ActionDispatch::Integration::Session.new(Rails.application)
session.post api_public_v1_dossiers_path(procedure),
headers: { "Content-Type" => "application/json" },
params: {
"champ_#{type_de_champ_text.to_typed_id}" => text_value,
"champ_#{type_de_champ_phone.to_typed_id}" => phone_value
}.to_json
JSON.parse(session.response.body)["dossier_url"].gsub("http://www.example.com", "")
end
end

View file

@ -1,80 +0,0 @@
describe 'Prefilling a dossier:' do
let(:password) { 'my-s3cure-p4ssword' }
let(:siret) { '41816609600051' }
let(:procedure) { create(:procedure, :published, opendata: true) }
let(:dossier) { procedure.dossiers.last }
let(:type_de_champ_text) { create(:type_de_champ_text, procedure: procedure) }
let(:type_de_champ_phone) { create(:type_de_champ_phone, procedure: procedure) }
let(:text_value) { "My Neighbor Totoro is the best movie ever" }
let(:phone_value) { "invalid phone value" }
before do
stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{siret}/)
.to_return(status: 200, body: File.read('spec/fixtures/files/api_entreprise/etablissements.json'))
visit commencer_path(
path: procedure.path,
"champ_#{type_de_champ_text.to_typed_id}" => text_value,
"champ_#{type_de_champ_phone.to_typed_id}" => phone_value
)
end
shared_examples "the user has got a prefilled dossier" do
scenario "the user has got a prefilled dossier" do
click_on "Commencer la démarche"
expect(page).to have_current_path siret_dossier_path(procedure.dossiers.last)
fill_in 'Numéro SIRET', with: siret
click_on 'Valider'
expect(page).to have_current_path(etablissement_dossier_path(dossier))
expect(page).to have_content('OCTO TECHNOLOGY')
click_on 'Continuer avec ces informations'
expect(page).to have_current_path(brouillon_dossier_path(dossier))
expect(page).to have_field(type_de_champ_text.libelle, with: text_value)
expect(page).to have_field(type_de_champ_phone.libelle, with: phone_value)
expect(page).to have_css('.field_with_errors', text: type_de_champ_phone.libelle)
end
end
context 'when the user signs in with email and password' do
it_behaves_like "the user has got a prefilled dossier" do
let!(:user) { create(:user, password: password) }
before do
click_on "Jai déjà un compte"
sign_in_with user.email, password
end
end
end
context 'when the user signs up with email and password' do
it_behaves_like "the user has got a prefilled dossier" do
let(:user_email) { generate :user_email }
before do
click_on "Créer un compte #{APPLICATION_NAME}"
sign_up_with user_email, password
expect(page).to have_content "nous avons besoin de vérifier votre adresse #{user_email}"
click_confirmation_link_for user_email
expect(page).to have_content('Votre compte a bien été confirmé.')
end
end
end
context 'when the user signs up with FranceConnect' do
it_behaves_like "the user has got a prefilled dossier" do
before do
allow_any_instance_of(FranceConnectParticulierClient).to receive(:authorization_uri).and_return(france_connect_particulier_callback_path(code: "c0d3"))
allow(FranceConnectService).to receive(:retrieve_user_informations_particulier).and_return(build(:france_connect_information))
page.find('.fr-connect').click
end
end
end
end