Merge pull request #3460 from betagouv/dev

2019-02-18-01
This commit is contained in:
LeSim 2019-02-18 18:00:57 +01:00 committed by GitHub
commit 6c6d5bbc92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 554 additions and 235 deletions

View file

@ -1,12 +1,13 @@
@import "colors";
.direct-upload { .direct-upload {
display: inline-block; display: inline-block;
position: relative; position: relative;
padding: 2px 4px; padding: 4px 15px;
margin: 0 3px 3px 0; margin: 0 3px 3px 0;
border: 1px solid rgba(0, 0, 0, 0.3); border: 1px solid $border-grey;
border-radius: 3px; border-radius: 3px;
font-size: 11px; font-size: 14px;
line-height: 13px;
} }
.direct-upload--pending { .direct-upload--pending {

View file

@ -111,6 +111,10 @@
} }
} }
.direct-upload {
margin-bottom: 2 * $default-padding;
}
.add-row { .add-row {
margin-bottom: 2 * $default-padding; margin-bottom: 2 * $default-padding;
} }

View file

@ -1,4 +1,6 @@
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include TrustedDeviceConcern
MAINTENANCE_MESSAGE = 'Le site est actuellement en maintenance. Il sera à nouveau disponible dans un court instant.' MAINTENANCE_MESSAGE = 'Le site est actuellement en maintenance. Il sera à nouveau disponible dans un court instant.'
# Prevent CSRF attacks by raising an exception. # Prevent CSRF attacks by raising an exception.
@ -6,6 +8,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, if: -> { !Rails.env.test? } protect_from_forgery with: :exception, if: -> { !Rails.env.test? }
before_action :load_navbar_left_pannel_partial_url before_action :load_navbar_left_pannel_partial_url
before_action :set_raven_context before_action :set_raven_context
before_action :redirect_if_untrusted
before_action :authorize_request_for_profiler before_action :authorize_request_for_profiler
before_action :reject, if: -> { Flipflop.maintenance_mode? } before_action :reject, if: -> { Flipflop.maintenance_mode? }
@ -151,4 +154,34 @@ class ApplicationController < ActionController::Base
redirect_to root_path redirect_to root_path
end end
end end
def redirect_if_untrusted
if gestionnaire_signed_in? &&
sensitive_path &&
current_gestionnaire.feature_enabled?(:enable_email_login_token) &&
!trusted_device?
# return at this location
# after the device is trusted
store_location_for(:user, request.fullpath)
send_login_token_or_bufferize(current_gestionnaire)
redirect_to link_sent_path(email: current_gestionnaire.email)
end
end
def sensitive_path
path = request.path_info
if path == '/' ||
path == '/users/sign_out' ||
path.start_with?('/connexion-par-jeton') ||
path.start_with?('/api/') ||
path.start_with?('/lien-envoye')
false
else
true
end
end
end end

View file

@ -29,6 +29,17 @@ module Manager
redirect_to manager_dossier_path(dossier) redirect_to manager_dossier_path(dossier)
end end
def hide
dossier = Dossier.find(params[:id])
deleted_dossier = dossier.hide!(current_administration)
DossierMailer.notify_deletion_to_user(deleted_dossier, dossier.user.email).deliver_later
logger.info("Le dossier #{dossier.id} est supprimé par #{current_administration.email}")
flash[:notice] = "Le dossier #{dossier.id} est supprimé"
redirect_to manager_dossier_path(dossier)
end
private private
def unfiltered_list? def unfiltered_list?

View file

@ -23,20 +23,7 @@ class Users::SessionsController < Sessions::SessionsController
current_user.update(loged_in_with_france_connect: nil) current_user.update(loged_in_with_france_connect: nil)
end end
if gestionnaire_signed_in? if gestionnaire_signed_in? || user_signed_in?
if trusted_device? || !current_gestionnaire.feature_enabled?(:enable_email_login_token)
set_flash_message :notice, :signed_in
redirect_to after_sign_in_path_for(:user)
else
gestionnaire = current_gestionnaire
send_login_token_or_bufferize(gestionnaire)
[:user, :gestionnaire, :administrateur].each { |role| sign_out(role) }
redirect_to link_sent_path(email: gestionnaire.email)
end
elsif user_signed_in?
set_flash_message :notice, :signed_in set_flash_message :notice, :signed_in
redirect_to after_sign_in_path_for(:user) redirect_to after_sign_in_path_for(:user)
else else
@ -83,32 +70,35 @@ class Users::SessionsController < Sessions::SessionsController
def sign_in_by_link def sign_in_by_link
gestionnaire = Gestionnaire.find(params[:id]) gestionnaire = Gestionnaire.find(params[:id])
if gestionnaire&.login_token_valid?(params[:jeton]) trusted_device_token = gestionnaire
trust_device .trusted_device_tokens
flash.notice = "Merci davoir confirmé votre connexion. Votre navigateur est maintenant authentifié pour #{TRUSTED_DEVICE_PERIOD.to_i / ActiveSupport::Duration::SECONDS_PER_DAY} jours." .find_by(token: params[:jeton])
user = User.find_by(email: gestionnaire.email) if trusted_device_token&.token_valid?
administrateur = Administrateur.find_by(email: gestionnaire.email) trust_device(trusted_device_token.created_at)
[user, gestionnaire, administrateur].compact.each { |resource| sign_in(resource) }
period = ((trusted_device_token.created_at + TRUSTED_DEVICE_PERIOD) - Time.zone.now).to_i / ActiveSupport::Duration::SECONDS_PER_DAY
flash.notice = "Merci davoir confirmé votre connexion. Votre navigateur est maintenant authentifié pour #{period} jours."
# redirect to procedure'url if stored by store_location_for(:user) in dossiers_controller # redirect to procedure'url if stored by store_location_for(:user) in dossiers_controller
# redirect to root_path otherwise # redirect to root_path otherwise
redirect_to after_sign_in_path_for(:user)
if gestionnaire_signed_in?
redirect_to after_sign_in_path_for(:user)
else
redirect_to new_user_session_path
end
else else
flash[:alert] = 'Votre lien est invalide ou expiré, veuillez-vous reconnecter.' flash[:alert] = 'Votre lien est invalide ou expiré, un nouveau vient de vous être envoyé.'
redirect_to new_user_session_path
send_login_token_or_bufferize(gestionnaire)
redirect_to link_sent_path(email: gestionnaire.email)
end end
end end
private private
def send_login_token_or_bufferize(gestionnaire)
if !gestionnaire.young_login_token?
login_token = gestionnaire.login_token!
GestionnaireMailer.send_login_token(gestionnaire, login_token).deliver_later
end
end
def try_to_authenticate(klass, remember_me = false) def try_to_authenticate(klass, remember_me = false)
resource = klass.find_for_database_authentication(email: params[:user][:email]) resource = klass.find_for_database_authentication(email: params[:user][:email])

View file

@ -1,6 +1,6 @@
require "administrate/field/base" require "administrate/field/base"
class ProcedureLinkField < Administrate::Field::Base class ProcedureLinkField < Administrate::Field::String
def name def name
"Lien démarche" "Lien démarche"
end end

View file

@ -1,5 +1,5 @@
import { getJSON, debounce } from '@utils'; import { getJSON, debounce } from '@utils';
import { DirectUpload } from 'activestorage'; import Uploader from '../../shared/activestorage/uploader';
export default { export default {
props: ['state', 'index', 'item'], props: ['state', 'index', 'item'],
@ -181,7 +181,12 @@ export default {
const file = input.files[0]; const file = input.files[0];
if (file) { if (file) {
this.isUploading = true; this.isUploading = true;
uploadFile(this.state.directUploadUrl, file).then(({ signed_id }) => { const controller = new Uploader(
input,
file,
this.state.directUploadUrl
);
controller.start().then(signed_id => {
this.pieceJustificativeTemplate = signed_id; this.pieceJustificativeTemplate = signed_id;
this.isUploading = false; this.isUploading = false;
this.debouncedSave(); this.debouncedSave();
@ -247,17 +252,3 @@ const EXCLUDE_FROM_REPETITION = [
function castBoolean(value) { function castBoolean(value) {
return value && value != 0; return value && value != 0;
} }
function uploadFile(directUploadUrl, file) {
const upload = new DirectUpload(file, directUploadUrl);
return new Promise((resolve, reject) => {
upload.create((error, blob) => {
if (error) {
reject(error);
} else {
resolve(blob);
}
});
});
}

View file

@ -4,7 +4,7 @@ import Rails from 'rails-ujs';
import * as ActiveStorage from 'activestorage'; import * as ActiveStorage from 'activestorage';
import jQuery from 'jquery'; import jQuery from 'jquery';
import '../shared/activestorage/progress'; import '../shared/activestorage/ujs';
import '../shared/sentry'; import '../shared/sentry';
import '../shared/rails-ujs-fix'; import '../shared/rails-ujs-fix';
import '../shared/safari-11-file-xhr-workaround'; import '../shared/safari-11-file-xhr-workaround';

View file

@ -5,7 +5,7 @@ import * as ActiveStorage from 'activestorage';
import Chartkick from 'chartkick'; import Chartkick from 'chartkick';
import Highcharts from 'highcharts'; import Highcharts from 'highcharts';
import '../shared/activestorage/progress'; import '../shared/activestorage/ujs';
import '../shared/sentry'; import '../shared/sentry';
import '../shared/rails-ujs-fix'; import '../shared/rails-ujs-fix';
import '../shared/safari-11-file-xhr-workaround'; import '../shared/safari-11-file-xhr-workaround';

View file

@ -0,0 +1,92 @@
const PENDING_CLASS = 'direct-upload--pending';
const ERROR_CLASS = 'direct-upload--error';
const COMPLETE_CLASS = 'direct-upload--complete';
/**
ProgressBar is and utility class responsible for
rendering upload progress bar. It is used to handle
direct-upload form ujs events but also in the
Uploader delegate used with uploads on json api.
*/
export default class ProgressBar {
static init(input, id, file) {
clearErrors(input);
const html = this.render(id, file.name);
input.insertAdjacentHTML('beforebegin', html);
}
static start(id) {
const element = getDirectUploadElement(id);
element.classList.remove(PENDING_CLASS);
}
static progress(id, progress) {
const element = getDirectUploadProgressElement(id);
element.style.width = `${progress}%`;
}
static error(id, error) {
const element = getDirectUploadElement(id);
element.classList.add(ERROR_CLASS);
element.setAttribute('title', error);
}
static end(id) {
const element = getDirectUploadElement(id);
element.classList.add(COMPLETE_CLASS);
}
static render(id, filename) {
return `<div id="direct-upload-${id}" class="direct-upload ${PENDING_CLASS}">
<div class="direct-upload__progress" style="width: 0%"></div>
<span class="direct-upload__filename">${filename}</span>
</div>`;
}
constructor(input, id, file) {
this.constructor.init(input, id, file);
this.id = id;
}
start() {
this.constructor.start(this.id);
}
progress(progress) {
this.constructor.progress(this.id, progress);
}
error(error) {
this.constructor.error(this.id, error);
}
end() {
this.constructor.end(this.id);
}
destroy() {
const element = getDirectUploadElement(this.id);
element.remove();
}
}
function clearErrors(input) {
const errorElements = input.parentElement.querySelectorAll(`.${ERROR_CLASS}`);
for (let element of errorElements) {
element.remove();
}
}
function getDirectUploadElement(id) {
return document.getElementById(`direct-upload-${id}`);
}
function getDirectUploadProgressElement(id) {
return document.querySelector(
`#direct-upload-${id} .direct-upload__progress`
);
}

View file

@ -1,49 +0,0 @@
addEventListener('direct-upload:initialize', event => {
const target = event.target,
detail = event.detail,
id = detail.id,
file = detail.file;
target.insertAdjacentHTML(
'beforebegin',
'\n<div id="direct-upload-' +
id +
'" class="direct-upload direct-upload--pending">\n<div id="direct-upload-progress-' +
id +
'" class="direct-upload__progress" style="width: 0%"></div>\n<span class="direct-upload__filename">' +
file.name +
'</span>\n</div>\n'
);
});
addEventListener('direct-upload:start', event => {
const id = event.detail.id,
element = document.getElementById('direct-upload-' + id);
element.classList.remove('direct-upload--pending');
});
addEventListener('direct-upload:progress', event => {
const id = event.detail.id,
progress = event.detail.progress,
progressElement = document.getElementById('direct-upload-progress-' + id);
progressElement.style.width = `${progress} %`;
});
addEventListener('direct-upload:error', event => {
event.preventDefault();
const id = event.detail.id,
error = event.detail.error,
element = document.getElementById('direct-upload-' + id);
element.classList.add('direct-upload--error');
element.setAttribute('title', error);
});
addEventListener('direct-upload:end', event => {
const id = event.detail.id,
element = document.getElementById('direct-upload-' + id);
element.classList.add('direct-upload--complete');
});

View file

@ -0,0 +1,27 @@
import ProgressBar from './progress-bar';
const INITIALIZE_EVENT = 'direct-upload:initialize';
const START_EVENT = 'direct-upload:start';
const PROGRESS_EVENT = 'direct-upload:progress';
const ERROR_EVENT = 'direct-upload:error';
const END_EVENT = 'direct-upload:end';
addEventListener(INITIALIZE_EVENT, ({ target, detail: { id, file } }) => {
ProgressBar.init(target, id, file);
});
addEventListener(START_EVENT, ({ detail: { id } }) => {
ProgressBar.start(id);
});
addEventListener(PROGRESS_EVENT, ({ detail: { id, progress } }) => {
ProgressBar.progress(id, progress);
});
addEventListener(ERROR_EVENT, ({ detail: { id, error } }) => {
ProgressBar.error(id, error);
});
addEventListener(END_EVENT, ({ detail: { id } }) => {
ProgressBar.end(id);
});

View file

@ -0,0 +1,43 @@
import { DirectUpload } from 'activestorage';
import ProgressBar from './progress-bar';
/**
Uploader class is a delegate for DirectUpload instance
used to track lifecycle and progress of un upload.
*/
export default class Uploader {
constructor(input, file, directUploadUrl) {
this.directUpload = new DirectUpload(file, directUploadUrl, this);
this.progressBar = new ProgressBar(input, this.directUpload.id, file);
}
start() {
this.progressBar.start();
return new Promise((resolve, reject) => {
this.directUpload.create((error, attributes) => {
if (error) {
this.progressBar.error(error);
reject(error);
} else {
resolve(attributes.signed_id);
}
this.progressBar.end();
this.progressBar.destroy();
});
});
}
uploadRequestDidProgress(event) {
const progress = (event.loaded / event.total) * 100;
if (progress) {
this.progressBar.progress(progress);
}
}
directUploadWillStoreFileWithXHR(xhr) {
xhr.upload.addEventListener('progress', event =>
this.uploadRequestDidProgress(event)
);
}
}

View file

@ -4,10 +4,10 @@ module TrustedDeviceConcern
TRUSTED_DEVICE_COOKIE_NAME = :trusted_device TRUSTED_DEVICE_COOKIE_NAME = :trusted_device
TRUSTED_DEVICE_PERIOD = 1.month TRUSTED_DEVICE_PERIOD = 1.month
def trust_device def trust_device(start_at)
cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = { cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = {
value: JSON.generate({ created_at: Time.zone.now }), value: JSON.generate({ created_at: start_at }),
expires: TRUSTED_DEVICE_PERIOD, expires: start_at + TRUSTED_DEVICE_PERIOD,
httponly: true httponly: true
} }
end end
@ -17,6 +17,13 @@ module TrustedDeviceConcern
(Time.zone.now - TRUSTED_DEVICE_PERIOD) < trusted_device_cookie_created_at (Time.zone.now - TRUSTED_DEVICE_PERIOD) < trusted_device_cookie_created_at
end end
def send_login_token_or_bufferize(gestionnaire)
if !gestionnaire.young_login_token?
login_token = gestionnaire.create_trusted_device_token
GestionnaireMailer.send_login_token(gestionnaire, login_token).deliver_later
end
end
private private
def trusted_device_cookie_created_at def trusted_device_cookie_created_at

View file

@ -1,3 +1,7 @@
class DeletedDossier < ApplicationRecord class DeletedDossier < ApplicationRecord
belongs_to :procedure belongs_to :procedure
def self.create_from_dossier(dossier)
DeletedDossier.create!(dossier_id: dossier.id, procedure: dossier.procedure, state: dossier.state, deleted_at: Time.now.utc)
end
end end

View file

@ -258,9 +258,8 @@ class Dossier < ApplicationRecord
end end
def delete_and_keep_track def delete_and_keep_track
now = Time.zone.now deleted_dossier = DeletedDossier.create_from_dossier(self)
deleted_dossier = DeletedDossier.create!(dossier_id: id, procedure: procedure, state: state, deleted_at: now) update(hidden_at: deleted_dossier.deleted_at)
update(hidden_at: now)
if en_construction? if en_construction?
administration_emails = followers_gestionnaires.present? ? followers_gestionnaires.pluck(:email) : [procedure.administrateur.email] administration_emails = followers_gestionnaires.present? ? followers_gestionnaires.pluck(:email) : [procedure.administrateur.email]
@ -318,6 +317,13 @@ class Dossier < ApplicationRecord
log_dossier_operation(nil, :accepter, automatic_operation: true) log_dossier_operation(nil, :accepter, automatic_operation: true)
end end
def hide!(administration)
update(hidden_at: Time.zone.now)
log_administration_dossier_operation(administration, :supprimer)
DeletedDossier.create_from_dossier(self)
end
def refuser!(gestionnaire, motivation) def refuser!(gestionnaire, motivation)
self.motivation = motivation self.motivation = motivation
self.en_instruction_at ||= Time.zone.now self.en_instruction_at ||= Time.zone.now
@ -356,6 +362,13 @@ class Dossier < ApplicationRecord
) )
end end
def log_administration_dossier_operation(administration, operation)
dossier_operation_logs.create(
administration: administration,
operation: DossierOperationLog.operations.fetch(operation)
)
end
def update_state_dates def update_state_dates
if en_construction? && !self.en_construction_at if en_construction? && !self.en_construction_at
self.en_construction_at = Time.zone.now self.en_construction_at = Time.zone.now

View file

@ -4,9 +4,11 @@ class DossierOperationLog < ApplicationRecord
repasser_en_construction: 'repasser_en_construction', repasser_en_construction: 'repasser_en_construction',
accepter: 'accepter', accepter: 'accepter',
refuser: 'refuser', refuser: 'refuser',
classer_sans_suite: 'classer_sans_suite' classer_sans_suite: 'classer_sans_suite',
supprimer: 'supprimer'
} }
belongs_to :dossier belongs_to :dossier
belongs_to :gestionnaire belongs_to :gestionnaire
belongs_to :administration
end end

View file

@ -1,10 +1,6 @@
class Gestionnaire < ApplicationRecord class Gestionnaire < ApplicationRecord
include CredentialsSyncableConcern include CredentialsSyncableConcern
include EmailSanitizableConcern include EmailSanitizableConcern
include ActiveRecord::SecureToken
LOGIN_TOKEN_VALIDITY = 45.minutes
LOGIN_TOKEN_YOUTH = 15.minutes
devise :database_authenticatable, :registerable, :async, devise :database_authenticatable, :registerable, :async,
:recoverable, :rememberable, :trackable, :validatable :recoverable, :rememberable, :trackable, :validatable
@ -20,6 +16,7 @@ class Gestionnaire < ApplicationRecord
has_many :followed_dossiers, through: :follows, source: :dossier has_many :followed_dossiers, through: :follows, source: :dossier
has_many :avis has_many :avis
has_many :dossiers_from_avis, through: :avis, source: :dossier has_many :dossiers_from_avis, through: :avis, source: :dossier
has_many :trusted_device_tokens
def visible_procedures def visible_procedures
procedures.merge(Procedure.avec_lien.or(Procedure.archivees)) procedures.merge(Procedure.avec_lien.or(Procedure.archivees))
@ -135,18 +132,9 @@ class Gestionnaire < ApplicationRecord
Dossier.where(id: dossiers_id_with_notifications(dossiers)).group(:procedure_id).count Dossier.where(id: dossiers_id_with_notifications(dossiers)).group(:procedure_id).count
end end
def login_token! def create_trusted_device_token
login_token = Gestionnaire.generate_unique_secure_token trusted_device_token = trusted_device_tokens.create
encrypted_login_token = BCrypt::Password.create(login_token) trusted_device_token.token
update(encrypted_login_token: encrypted_login_token, login_token_created_at: Time.zone.now)
login_token
end
def login_token_valid?(login_token)
BCrypt::Password.new(encrypted_login_token) == login_token &&
LOGIN_TOKEN_VALIDITY.ago < login_token_created_at
rescue BCrypt::Errors::InvalidHash
false
end end
def dossiers_id_with_notifications(dossiers) def dossiers_id_with_notifications(dossiers)
@ -213,8 +201,8 @@ class Gestionnaire < ApplicationRecord
end end
def young_login_token? def young_login_token?
login_token_created_at.present? && trusted_device_token = trusted_device_tokens.order(created_at: :desc).first
LOGIN_TOKEN_YOUTH.ago < login_token_created_at trusted_device_token&.token_young?
end end
private private

View file

@ -7,11 +7,10 @@ class Service < ApplicationRecord
enum type_organisme: { enum type_organisme: {
administration_centrale: 'administration_centrale', administration_centrale: 'administration_centrale',
association: 'association', association: 'association',
commune: 'commune', collectivite_territoriale: 'collectivite_territoriale',
departement: 'departement',
etablissement_enseignement: 'etablissement_enseignement', etablissement_enseignement: 'etablissement_enseignement',
prefecture: 'prefecture', operateur_d_etat: "operateur_d_etat",
region: 'region', service_deconcentre_de_l_etat: 'service_deconcentre_de_l_etat',
autre: 'autre' autre: 'autre'
} }

View file

@ -0,0 +1,15 @@
class TrustedDeviceToken < ApplicationRecord
LOGIN_TOKEN_VALIDITY = 1.week
LOGIN_TOKEN_YOUTH = 15.minutes
belongs_to :gestionnaire
has_secure_token
def token_valid?
LOGIN_TOKEN_VALIDITY.ago < created_at
end
def token_young?
LOGIN_TOKEN_YOUTH.ago < created_at
end
end

View file

@ -10,7 +10,11 @@
- else - else
%small Désactivée %small Désactivée
%p.notice Les attestations, si elles sont activées, sont délivrées par email aux usagers lorsque leurs dossiers sont acceptés, et sont également disponibles au téléchargement sur leur espace personnel. %p.notice
Lattestation, si elle est activée, est émise au moment où un dossier est accepté.
%br
Lemail daccusé dacceptation envoyé à lusager comporte alors un lien vers lattestation ;
celle-ci est également disponible au téléchargement depuis lespace personnel de lusager.
.image-upload .image-upload
- if @attestation_template.logo.present? - if @attestation_template.logo.present?

View file

@ -4,14 +4,20 @@
Bonjour, Bonjour,
%p %p
Les comptes administrateurs sont destinés aux administrations publiques souhaitant mettre en place des démarches dématérialisées avec demarches-simplifiees.fr. Il ne semble pas que ce soit votre cas. Les comptes administrateurs sont destinés aux administrations publiques souhaitant mettre en place sur leurs sites internet des démarches dématérialisées pour leurs usagers. Il ne semble pas que ce soit votre cas.
%p %p
Pour les usagers qui souhaitent remplir une démarche, lentrée dans demarches-simplifiees.fr se fait via un lien fourni par ladministration responsable, sur son propre site web. Ce lien vous permettra de créer un compte et de remplir le formulaire dans la foulée. Pour les usagers ou les administrations publiques (collectivités, etc.) qui souhaitent remplir une démarche ou un déposer un dossier en ligne, lentrée dans demarches-simplifiees.fr se fait via un lien fourni par ladministration responsable, sur son propre site web. Ce lien vous permettra de créer un compte et de remplir le formulaire dans la foulée.
%p %p
Si par contre vous rencontrez des problèmes lors de l'utilisation de demarches-simplifiees.fr en tant qu'usager, merci d'expliciter le problème rencontré sur notre Si par contre vous rencontrez des problèmes lors de l'utilisation de demarches-simplifiees.fr en tant qu'usager, merci d'expliciter le problème rencontré sur notre
= link_to("formulaire de contact", contact_url) = link_to("formulaire de contact", contact_url)
\. \.
%p
Si vous avez fait une demande de compte administrateur légitime avec une adresse email grand public (Orange, Wanadoo etc), merci de nous contacter sur notre
= link_to("formulaire de contact administrateur", contact_admin_url)
\.
= render partial: "layouts/mailers/signature" = render partial: "layouts/mailers/signature"

View file

@ -23,7 +23,7 @@ as well as a link to its edit page.
<h1 class="main-content__page-title"> <h1 class="main-content__page-title">
<%= content_for(:title) %> <%= content_for(:title) %>
<% if dossier.hidden_at %> <% if dossier.hidden_at %>
(SUPPRIMÉ) (Supprimé)
<% end %> <% end %>
</h1> </h1>
@ -31,6 +31,9 @@ as well as a link to its edit page.
<% if dossier.termine? %> <% if dossier.termine? %>
<%= link_to 'Repasser en instruction', change_state_to_instruction_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Repasser en instruction ?" } %> <%= link_to 'Repasser en instruction', change_state_to_instruction_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Repasser en instruction ?" } %>
<% end %> <% end %>
<% if dossier.hidden_at.nil? %>
<%= link_to 'Supprimer le dossier', hide_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Confirmez vous la suppression du dossier ?" } %>
<% end %>
<div> <div>
</header> </header>

View file

@ -33,6 +33,8 @@ as well as a link to its edit page.
</div> </div>
<div> <div>
<%= link_to 'aperçu', apercu_procedure_path(procedure), class: 'button' %>
<% if !procedure.whitelisted? %> <% if !procedure.whitelisted? %>
<%= link_to 'whitelister', whitelist_manager_procedure_path(procedure), method: :post, class: 'button' %> <%= link_to 'whitelister', whitelist_manager_procedure_path(procedure), method: :post, class: 'button' %>
<% end %> <% end %>

View file

@ -5,7 +5,10 @@
.title-container .title-container
%span.icon.folder %span.icon.folder
%h1= dossier.procedure.libelle %h1= dossier.procedure.libelle
%h2 Dossier nº #{dossier.id} - Déposé le #{l(dossier.en_construction_at, format: '%d %B %Y')} %h2
Dossier nº #{dossier.id}
- if dossier.en_construction_at.present?
= "- Déposé le #{l(dossier.en_construction_at, format: '%d %B %Y')}"
- if current_user.owns?(dossier) - if current_user.owns?(dossier)
.header-actions .header-actions

View file

@ -75,18 +75,18 @@
name: :save_draft, name: :save_draft,
value: true, value: true,
class: 'button send secondary', class: 'button send secondary',
data: { disable: true } data: { 'disable-with': "Envoi en cours…" }
- if dossier.can_transition_to_en_construction? - if dossier.can_transition_to_en_construction?
= f.button 'Soumettre le dossier', = f.button 'Soumettre le dossier',
class: 'button send primary', class: 'button send primary',
disabled: !current_user.owns?(dossier), disabled: !current_user.owns?(dossier),
data: { disable: true } data: { 'disable-with': "Envoi en cours…" }
- else - else
= f.button 'Enregistrer les modifications du dossier', = f.button 'Enregistrer les modifications du dossier',
class: 'button send primary', class: 'button send primary',
data: { disable: true } data: { 'disable-with': "Envoi en cours…" }
- if dossier.brouillon? && !current_user.owns?(dossier) - if dossier.brouillon? && !current_user.owns?(dossier)
.send-notice.invite-cannot-submit .send-notice.invite-cannot-submit

View file

@ -1,3 +1,4 @@
= form.select :value, = form.select :value,
Champs::DepartementChamp.departements, Champs::DepartementChamp.departements,
include_blank: true,
required: champ.mandatory? required: champ.mandatory?

View file

@ -1,10 +1,9 @@
fr: fr:
type_organisme: type_organisme:
administration_centrale: 'administration centrale' administration_centrale: 'Administration centrale'
association: 'association' association: 'Association'
commune: 'commune' collectivite_territoriale: 'Collectivité territoriale'
departement: 'département' etablissement_enseignement: 'Établissement denseignement'
etablissement_enseignement: 'établissement denseignement' operateur_d_etat: "Opérateur d'État"
prefecture: 'préfecture' service_deconcentre_de_l_etat: "Service déconcentré de l'État"
region: 'région' autre: 'Autre'
autre: 'autre'

View file

@ -14,6 +14,7 @@ Rails.application.routes.draw do
resources :dossiers, only: [:index, :show] do resources :dossiers, only: [:index, :show] do
post 'change_state_to_instruction', on: :member post 'change_state_to_instruction', on: :member
post 'hide', on: :member
end end
resources :administrateurs, only: [:index, :show, :new, :create] do resources :administrateurs, only: [:index, :show, :new, :create] do

View file

@ -0,0 +1,11 @@
class CreateTrustedDeviceTokens < ActiveRecord::Migration[5.2]
def change
create_table :trusted_device_tokens do |t|
t.string :token, null: false
t.references :gestionnaire, foreign_key: true
t.timestamps
end
add_index :trusted_device_tokens, :token, unique: true
end
end

View file

@ -0,0 +1,5 @@
class AddAdministrationColumnToLogDossierOperation < ActiveRecord::Migration[5.2]
def change
add_reference :dossier_operation_logs, :administration, foreign_key: true
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_01_10_163655) do ActiveRecord::Schema.define(version: 2019_02_13_144145) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -219,6 +219,8 @@ ActiveRecord::Schema.define(version: 2019_01_10_163655) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "automatic_operation", default: false, null: false t.boolean "automatic_operation", default: false, null: false
t.bigint "administration_id"
t.index ["administration_id"], name: "index_dossier_operation_logs_on_administration_id"
t.index ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id" t.index ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id"
t.index ["gestionnaire_id"], name: "index_dossier_operation_logs_on_gestionnaire_id" t.index ["gestionnaire_id"], name: "index_dossier_operation_logs_on_gestionnaire_id"
end end
@ -518,6 +520,15 @@ ActiveRecord::Schema.define(version: 2019_01_10_163655) do
t.string "version", null: false t.string "version", null: false
end end
create_table "trusted_device_tokens", force: :cascade do |t|
t.string "token", null: false
t.bigint "gestionnaire_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["gestionnaire_id"], name: "index_trusted_device_tokens_on_gestionnaire_id"
t.index ["token"], name: "index_trusted_device_tokens_on_token", unique: true
end
create_table "types_de_champ", id: :serial, force: :cascade do |t| create_table "types_de_champ", id: :serial, force: :cascade do |t|
t.string "libelle" t.string "libelle"
t.string "type_champ" t.string "type_champ"
@ -597,6 +608,7 @@ ActiveRecord::Schema.define(version: 2019_01_10_163655) do
add_foreign_key "champs", "champs", column: "parent_id" add_foreign_key "champs", "champs", column: "parent_id"
add_foreign_key "closed_mails", "procedures" add_foreign_key "closed_mails", "procedures"
add_foreign_key "commentaires", "dossiers" add_foreign_key "commentaires", "dossiers"
add_foreign_key "dossier_operation_logs", "administrations"
add_foreign_key "dossier_operation_logs", "dossiers" add_foreign_key "dossier_operation_logs", "dossiers"
add_foreign_key "dossier_operation_logs", "gestionnaires" add_foreign_key "dossier_operation_logs", "gestionnaires"
add_foreign_key "dossiers", "users" add_foreign_key "dossiers", "users"
@ -608,6 +620,7 @@ ActiveRecord::Schema.define(version: 2019_01_10_163655) do
add_foreign_key "received_mails", "procedures" add_foreign_key "received_mails", "procedures"
add_foreign_key "refused_mails", "procedures" add_foreign_key "refused_mails", "procedures"
add_foreign_key "services", "administrateurs" add_foreign_key "services", "administrateurs"
add_foreign_key "trusted_device_tokens", "gestionnaires"
add_foreign_key "types_de_champ", "types_de_champ", column: "parent_id" add_foreign_key "types_de_champ", "types_de_champ", column: "parent_id"
add_foreign_key "without_continuation_mails", "procedures" add_foreign_key "without_continuation_mails", "procedures"
end end

View file

@ -0,0 +1,17 @@
namespace :after_party do
desc 'Deployment task: migrate service organisme'
task migrate_service_organisme: :environment do
table = {
'commune': 'collectivite_territoriale',
'departement': 'collectivite_territoriale',
'region': 'collectivite_territoriale',
'prefecture': 'service_deconcentre_de_l_etat'
}
table.each do |(old_name, new_name)|
Service.where(type_organisme: old_name).update_all(type_organisme: new_name)
end
AfterParty::TaskRecord.create version: '20190201121252'
end
end

View file

@ -9,6 +9,7 @@ describe ApplicationController, type: :controller do
.map(&:filter) .map(&:filter)
expect(before_actions).to include(:set_raven_context) expect(before_actions).to include(:set_raven_context)
expect(before_actions).to include(:redirect_if_untrusted)
end end
end end
@ -145,4 +146,56 @@ describe ApplicationController, type: :controller do
it { expect(flash[:alert]).to eq(ApplicationController::MAINTENANCE_MESSAGE) } it { expect(flash[:alert]).to eq(ApplicationController::MAINTENANCE_MESSAGE) }
end end
end end
describe '#redirect_if_unstrusted' do
let(:current_gestionnaire) { create(:gestionnaire) }
before do
allow(current_gestionnaire).to receive(:feature_enabled?).and_return(feature_enabled)
allow(@controller).to receive(:current_gestionnaire).and_return(current_gestionnaire)
allow(@controller).to receive(:redirect_to)
allow(@controller).to receive(:trusted_device?).and_return(trusted_device)
allow(@controller).to receive(:gestionnaire_signed_in?).and_return(gestionnaire_signed_in)
allow(@controller).to receive(:sensitive_path).and_return(sensitive_path)
allow(@controller).to receive(:send_login_token_or_bufferize)
allow(@controller).to receive(:store_location_for)
end
subject { @controller.send(:redirect_if_untrusted) }
context 'when the path is sensitive' do
let(:sensitive_path) { true }
context 'when the gestionnaire is signed_in' do
let(:gestionnaire_signed_in) { true }
context 'when the feature is activated' do
let(:feature_enabled) { true }
context 'when the device is trusted' do
let(:trusted_device) { true }
before { subject }
it { expect(@controller).not_to have_received(:redirect_to) }
end
end
context 'when the feature is activated' do
let(:feature_enabled) { true }
context 'when the device is not trusted' do
let(:trusted_device) { false }
before { subject }
it { expect(@controller).to have_received(:redirect_to) }
it { expect(@controller).to have_received(:send_login_token_or_bufferize) }
it { expect(@controller).to have_received(:store_location_for) }
end
end
end
end
end
end end

View file

@ -0,0 +1,14 @@
describe Manager::DossiersController, type: :controller do
describe '#hide' do
let(:administration) { create :administration }
let!(:dossier) { create(:dossier) }
before do
sign_in administration
post :hide, params: { id: dossier.id }
dossier.reload
end
it { expect(dossier.hidden_at).not_to be_nil }
end
end

View file

@ -15,7 +15,7 @@ describe NewAdministrateur::ServicesController, type: :controller do
nom: 'super service', nom: 'super service',
organisme: 'organisme', organisme: 'organisme',
siret: '01234567891234', siret: '01234567891234',
type_organisme: 'region', type_organisme: 'association',
email: 'email@toto.com', email: 'email@toto.com',
telephone: '1234', telephone: '1234',
horaires: 'horaires', horaires: 'horaires',
@ -30,7 +30,7 @@ describe NewAdministrateur::ServicesController, type: :controller do
it { expect(Service.last.nom).to eq('super service') } it { expect(Service.last.nom).to eq('super service') }
it { expect(Service.last.organisme).to eq('organisme') } it { expect(Service.last.organisme).to eq('organisme') }
it { expect(Service.last.siret).to eq('01234567891234') } it { expect(Service.last.siret).to eq('01234567891234') }
it { expect(Service.last.type_organisme).to eq(Service.type_organismes.fetch(:region)) } it { expect(Service.last.type_organisme).to eq(Service.type_organismes.fetch(:association)) }
it { expect(Service.last.email).to eq('email@toto.com') } it { expect(Service.last.email).to eq('email@toto.com') }
it { expect(Service.last.telephone).to eq('1234') } it { expect(Service.last.telephone).to eq('1234') }
it { expect(Service.last.horaires).to eq('horaires') } it { expect(Service.last.horaires).to eq('horaires') }
@ -49,7 +49,7 @@ describe NewAdministrateur::ServicesController, type: :controller do
describe '#update' do describe '#update' do
let!(:service) { create(:service, administrateur: admin) } let!(:service) { create(:service, administrateur: admin) }
let(:service_params) { { nom: 'nom', type_organisme: Service.type_organismes.fetch(:region) } } let(:service_params) { { nom: 'nom', type_organisme: Service.type_organismes.fetch(:association) } }
before do before do
sign_in admin sign_in admin
@ -65,12 +65,12 @@ describe NewAdministrateur::ServicesController, type: :controller do
it { expect(flash.alert).to be_nil } it { expect(flash.alert).to be_nil }
it { expect(flash.notice).to eq('nom modifié') } it { expect(flash.notice).to eq('nom modifié') }
it { expect(Service.last.nom).to eq('nom') } it { expect(Service.last.nom).to eq('nom') }
it { expect(Service.last.type_organisme).to eq(Service.type_organismes.fetch(:region)) } it { expect(Service.last.type_organisme).to eq(Service.type_organismes.fetch(:association)) }
it { expect(response).to redirect_to(services_path(procedure_id: procedure.id)) } it { expect(response).to redirect_to(services_path(procedure_id: procedure.id)) }
end end
context 'when updating a service with invalid data' do context 'when updating a service with invalid data' do
let(:service_params) { { nom: '', type_organisme: Service.type_organismes.fetch(:region) } } let(:service_params) { { nom: '', type_organisme: Service.type_organismes.fetch(:association) } }
it { expect(flash.alert).not_to be_nil } it { expect(flash.alert).not_to be_nil }
it { expect(response).to render_template(:edit) } it { expect(response).to render_template(:edit) }

View file

@ -40,6 +40,7 @@ describe Sessions::SessionsController, type: :controller do
@request.env["devise.mapping"] = Devise.mappings[:gestionnaire] @request.env["devise.mapping"] = Devise.mappings[:gestionnaire]
allow_any_instance_of(described_class).to receive(:gestionnaire_signed_in?).and_return(true) allow_any_instance_of(described_class).to receive(:gestionnaire_signed_in?).and_return(true)
allow_any_instance_of(described_class).to receive(:current_gestionnaire).and_return(gestionnaire)
end end
it 'calls sign out for gestionnaire' do it 'calls sign out for gestionnaire' do

View file

@ -28,31 +28,15 @@ describe Users::SessionsController, type: :controller do
context 'when the device is not trusted' do context 'when the device is not trusted' do
let(:trusted_device) { false } let(:trusted_device) { false }
it 'redirects to the confirmation link path' do it 'redirects to the root path' do
subject subject
expect(controller).to redirect_to link_sent_path(email: email) expect(controller).to redirect_to(root_path)
# do not know why, should be test related
expect(controller.current_user).to eq(user) expect(controller.current_user).to eq(user)
expect(controller.current_gestionnaire).to eq(gestionnaire)
expect(controller.current_gestionnaire).to be(nil) expect(controller.current_administrateur).to eq(administrateur)
expect(controller.current_administrateur).to be(nil) expect(user.loged_in_with_france_connect).to eq(nil)
expect(user.loged_in_with_france_connect).to be(nil)
expect(GestionnaireMailer).to have_received(:send_login_token)
end
context 'and the user try to connect multiple times in a short period' do
before do
allow_any_instance_of(Gestionnaire).to receive(:young_login_token?).and_return(true)
allow(GestionnaireMailer).to receive(:send_login_token)
end
it 'does not renew nor send a new login token' do
subject
expect(GestionnaireMailer).not_to have_received(:send_login_token)
end
end end
end end
@ -69,7 +53,6 @@ describe Users::SessionsController, type: :controller do
expect(controller.current_gestionnaire).to eq(gestionnaire) expect(controller.current_gestionnaire).to eq(gestionnaire)
expect(controller.current_administrateur).to eq(administrateur) expect(controller.current_administrateur).to eq(administrateur)
expect(user.loged_in_with_france_connect).to be(nil) expect(user.loged_in_with_france_connect).to be(nil)
expect(GestionnaireMailer).not_to have_received(:send_login_token)
end end
end end
@ -192,50 +175,57 @@ describe Users::SessionsController, type: :controller do
describe '#sign_in_by_link' do describe '#sign_in_by_link' do
context 'when the gestionnaire has non other account' do context 'when the gestionnaire has non other account' do
let(:gestionnaire) { create(:gestionnaire) } let(:gestionnaire) { create(:gestionnaire) }
let!(:good_jeton) { gestionnaire.login_token! } let!(:good_jeton) { gestionnaire.create_trusted_device_token }
let(:logged) { false }
before do before do
if logged
sign_in gestionnaire
end
allow(controller).to receive(:trust_device) allow(controller).to receive(:trust_device)
allow(controller).to receive(:send_login_token_or_bufferize)
post :sign_in_by_link, params: { id: gestionnaire.id, jeton: jeton } post :sign_in_by_link, params: { id: gestionnaire.id, jeton: jeton }
end end
context 'when the token is valid' do context 'when the gestionnaire is not logged in' do
let(:jeton) { good_jeton } context 'when the token is valid' do
let(:jeton) { good_jeton }
# TODO when the gestionnaire has no other account, and the token is valid, and the user signing in was not starting a demarche, it { is_expected.to redirect_to new_user_session_path }
# redirect to root_path, then redirect to gestionnaire_procedures_path (see root_controller) it { expect(controller.current_gestionnaire).to be_nil }
it { is_expected.to redirect_to root_path } it { expect(controller).to have_received(:trust_device) }
it { expect(controller.current_gestionnaire).to eq(gestionnaire) } end
it { expect(controller).to have_received(:trust_device) }
context 'when the token is invalid' do
let(:jeton) { 'invalid_token' }
it { is_expected.to redirect_to link_sent_path(email: gestionnaire.email) }
it { expect(controller.current_gestionnaire).to be_nil }
it { expect(controller).not_to have_received(:trust_device) }
it { expect(controller).to have_received(:send_login_token_or_bufferize) }
end
end end
context 'when the token is invalid' do context 'when the gestionnaire is logged in' do
let(:jeton) { 'invalid_token' } let(:logged) { true }
it { is_expected.to redirect_to new_user_session_path } context 'when the token is valid' do
it { expect(controller.current_gestionnaire).to be_nil } let(:jeton) { good_jeton }
it { expect(controller).not_to have_received(:trust_device) }
end
end
context 'when the gestionnaire has an user and admin account' do # redirect to root_path, then redirect to gestionnaire_procedures_path (see root_controller)
let(:email) { 'unique@plop.com' } it { is_expected.to redirect_to root_path }
let(:password) { 'un super mot de passe' } it { expect(controller.current_gestionnaire).to eq(gestionnaire) }
it { expect(controller).to have_received(:trust_device) }
end
let!(:user) { create(:user, email: email, password: password) } context 'when the token is invalid' do
let!(:administrateur) { create(:administrateur, email: email, password: password) } let(:jeton) { 'invalid_token' }
let(:gestionnaire) { administrateur.gestionnaire }
before do it { is_expected.to redirect_to link_sent_path(email: gestionnaire.email) }
post :sign_in_by_link, params: { id: gestionnaire.id, jeton: jeton } it { expect(controller.current_gestionnaire).to eq(gestionnaire) }
end it { expect(controller).not_to have_received(:trust_device) }
it { expect(controller).to have_received(:send_login_token_or_bufferize) }
context 'when the token is valid' do end
let(:jeton) { gestionnaire.login_token! }
it { expect(controller.current_gestionnaire).to eq(gestionnaire) }
it { expect(controller.current_administrateur).to eq(administrateur) }
it { expect(controller.current_user).to eq(user) }
end end
end end
end end
@ -249,16 +239,15 @@ describe Users::SessionsController, type: :controller do
context 'when the cookie is outdated' do context 'when the cookie is outdated' do
before do before do
Timecop.freeze(Time.zone.now - TrustedDeviceConcern::TRUSTED_DEVICE_PERIOD - 1.minute) emission_date = Time.zone.now - TrustedDeviceConcern::TRUSTED_DEVICE_PERIOD - 1.minute
controller.trust_device controller.trust_device(emission_date)
Timecop.return
end end
it { is_expected.to be false } it { is_expected.to be false }
end end
context 'when the cookie is ok' do context 'when the cookie is ok' do
before { controller.trust_device } before { controller.trust_device(Time.zone.now) }
it { is_expected.to be true } it { is_expected.to be true }
end end

View file

@ -2,7 +2,7 @@ FactoryBot.define do
factory :service do factory :service do
nom { 'service' } nom { 'service' }
organisme { 'organisme' } organisme { 'organisme' }
type_organisme { Service.type_organismes.fetch(:commune) } type_organisme { Service.type_organismes.fetch(:association) }
administrateur { create(:administrateur) } administrateur { create(:administrateur) }
email { 'email@toto.com' } email { 'email@toto.com' }
telephone { '1234' } telephone { '1234' }

View file

@ -892,4 +892,21 @@ describe Dossier do
end end
end end
end end
describe '#hide!' do
let(:dossier) { create(:dossier) }
let(:administration) { create(:administration) }
let(:last_operation) { dossier.dossier_operation_logs.last }
before do
Timecop.freeze
dossier.hide!(administration)
end
after { Timecop.return }
it { expect(dossier.hidden_at).to eq(Time.zone.now) }
it { expect(last_operation.operation).to eq('supprimer') }
it { expect(last_operation.administration).to eq(administration) }
end
end end

View file

@ -392,44 +392,24 @@ describe Gestionnaire, type: :model do
end end
end end
describe '#login_token_valid?' do
let!(:gestionnaire) { create(:gestionnaire) }
let!(:good_token) { gestionnaire.login_token! }
it { expect(gestionnaire.login_token_valid?(good_token)).to be true }
it { expect(gestionnaire.login_token_valid?('bad_token')).to be false }
context 'when the token as expired' do
before { gestionnaire.update(login_token_created_at: (Gestionnaire::LOGIN_TOKEN_VALIDITY + 1.minute).ago) }
it { expect(gestionnaire.login_token_valid?(good_token)).to be false }
end
context 'when the gestionnaire does not have a token' do
before { gestionnaire.update(encrypted_login_token: nil) }
it { expect(gestionnaire.login_token_valid?(nil)).to be false }
end
end
describe '#young_login_token?' do describe '#young_login_token?' do
let!(:gestionnaire) { create(:gestionnaire) } let!(:gestionnaire) { create(:gestionnaire) }
context 'when there is a token' do context 'when there is a token' do
let!(:good_token) { gestionnaire.login_token! } let!(:good_token) { gestionnaire.create_trusted_device_token }
context 'when the token has just been created' do context 'when the token has just been created' do
it { expect(gestionnaire.young_login_token?).to be true } it { expect(gestionnaire.young_login_token?).to be true }
end end
context 'when the token is a bit old' do context 'when the token is a bit old' do
before { gestionnaire.update(login_token_created_at: (Gestionnaire::LOGIN_TOKEN_YOUTH + 1.minute).ago) } before { gestionnaire.trusted_device_tokens.first.update(created_at: (TrustedDeviceToken::LOGIN_TOKEN_YOUTH + 1.minute).ago) }
it { expect(gestionnaire.young_login_token?).to be false } it { expect(gestionnaire.young_login_token?).to be false }
end end
end end
context 'when there are no token' do context 'when there are no token' do
it { expect(gestionnaire.young_login_token?).to be false } it { expect(gestionnaire.young_login_token?).to be_falsey }
end end
end end

View file

@ -5,7 +5,7 @@ describe Service, type: :model do
{ {
nom: 'service des jardins', nom: 'service des jardins',
organisme: 'mairie des iles', organisme: 'mairie des iles',
type_organisme: Service.type_organismes.fetch(:commune), type_organisme: Service.type_organismes.fetch(:association),
email: 'super@email.com', email: 'super@email.com',
telephone: '1212202', telephone: '1212202',
horaires: 'du lundi au vendredi', horaires: 'du lundi au vendredi',

View file

@ -0,0 +1,29 @@
RSpec.describe TrustedDeviceToken, type: :model do
describe '#token_valid?' do
let(:token) { TrustedDeviceToken.create }
context 'when the token is create after login_token_validity' do
it { expect(token.token_valid?).to be true }
end
context 'when the token is create before login_token_validity' do
before { token.update(created_at: (TrustedDeviceToken::LOGIN_TOKEN_VALIDITY + 1.minute).ago) }
it { expect(token.token_valid?).to be false }
end
end
describe '#token_young?' do
let(:token) { TrustedDeviceToken.create }
context 'when the token is create after login_token_youth' do
it { expect(token.token_young?).to be true }
end
context 'when the token is create before login_token_youth' do
before { token.update(created_at: (TrustedDeviceToken::LOGIN_TOKEN_YOUTH + 1.minute).ago) }
it { expect(token.token_young?).to be false }
end
end
end