commit
9304c4faf9
43 changed files with 719 additions and 55 deletions
|
@ -10,5 +10,5 @@ indent_size = 2
|
|||
indent_style = space
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{ico,keep,pdf,svg}]
|
||||
[*.{ico,keep,pdf,svg,der}]
|
||||
insert_final_newline = false
|
||||
|
|
|
@ -726,7 +726,7 @@ Rails/HttpStatus:
|
|||
Enabled: false
|
||||
|
||||
Rails/InverseOf:
|
||||
Enabled: false
|
||||
Enabled: true
|
||||
|
||||
Rails/LexicallyScopedActionFilter:
|
||||
Enabled: false
|
||||
|
|
|
@ -45,8 +45,6 @@ demarches-simplifiees.fr est **compliqué à héberger**. Parmi les problématiq
|
|||
|
||||
Si vous souhaitez adapter demarches-simplifiees.fr à votre besoin, nous vous recommandons de **proposer vos modifications à la base de code principale** (par exemple en créant une issue) **plutôt que d’héberger une autre instance vous-même**.
|
||||
|
||||
Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation.
|
||||
Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d’assurer de support technique concernant votre installation.
|
||||
|
||||
Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation.
|
||||
|
||||
Totefois, le ministère des armées a déployé une instance au sein de leur intranet. Nous proposons aux acteurs qui sont interessés de les mettre en relation avec eux afin de disposer d'un retour d'expérience, et bénéficier de leur retour.
|
||||
Toutefois, certains acteurs (le ministère des armées, l’administration autonome en Polynésie française) ont déployé des instances séparées. Nous proposons aux personnes intéressées de les mettre en relation avec ces acteurs existants, afin de disposer d’un retour d’expérience, et de bénéficier de leur retour.
|
||||
|
|
|
@ -70,6 +70,7 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian
|
|||
WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later
|
||||
GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later
|
||||
PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later
|
||||
OperationsSignatureJob.set(cron: "0 6 * * *").perform_later
|
||||
|
||||
### Voir les emails envoyés en local
|
||||
|
||||
|
|
4
app/controllers/manager/bill_signatures_controller.rb
Normal file
4
app/controllers/manager/bill_signatures_controller.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
module Manager
|
||||
class BillSignaturesController < Manager::ApplicationController
|
||||
end
|
||||
end
|
22
app/dashboards/bill_signature_dashboard.rb
Normal file
22
app/dashboards/bill_signature_dashboard.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
require "administrate/base_dashboard"
|
||||
|
||||
class BillSignatureDashboard < Administrate::BaseDashboard
|
||||
ATTRIBUTE_TYPES = {
|
||||
dossier_operation_logs: Field::HasMany,
|
||||
id: Field::Number,
|
||||
digest: Field::String,
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime,
|
||||
serialized: AttachmentField,
|
||||
signature: AttachmentField
|
||||
}.freeze
|
||||
|
||||
COLLECTION_ATTRIBUTES = [
|
||||
:id,
|
||||
:created_at,
|
||||
:dossier_operation_logs,
|
||||
:digest,
|
||||
:serialized,
|
||||
:signature
|
||||
].freeze
|
||||
end
|
11
app/fields/attachment_field.rb
Normal file
11
app/fields/attachment_field.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
require "administrate/field/base"
|
||||
|
||||
class AttachmentField < Administrate::Field::Base
|
||||
def to_s
|
||||
data.filename.to_s
|
||||
end
|
||||
|
||||
def blob_path
|
||||
Rails.application.routes.url_helpers.rails_blob_path(data)
|
||||
end
|
||||
end
|
15
app/jobs/operations_signature_job.rb
Normal file
15
app/jobs/operations_signature_job.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
class OperationsSignatureJob < ApplicationJob
|
||||
queue_as :cron
|
||||
|
||||
def perform(*args)
|
||||
last_midnight = Time.zone.today.beginning_of_day
|
||||
operations_by_day = BillSignatureService.grouped_unsigned_operation_until(last_midnight)
|
||||
operations_by_day.each do |day, operations|
|
||||
begin
|
||||
BillSignatureService.sign_operations(operations, day)
|
||||
rescue
|
||||
raise # let errors show up on Sentry and delayed_jobs
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
25
app/lib/asn1/timestamp.rb
Normal file
25
app/lib/asn1/timestamp.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
class ASN1::Timestamp
|
||||
## Poor man’s rfc3161 timestamp decoding
|
||||
# This works, as of 2019-05, for timestamps delivered by the universign POST api.
|
||||
# We should properly access the ASN1 contents using the sequence and tags structure.
|
||||
# However:
|
||||
# * It’s hard to do right.
|
||||
# * We currently don’t require it for proper operation; timestamps are never exposed to users.
|
||||
# * There’s an ongoing PR https://github.com/ruby/openssl/pull/204 for proper timestamp decoding in the ruby openssl library; let’s use OpenSSL::TS once it exists.
|
||||
|
||||
def self.timestampInfo(asn1timestamp)
|
||||
asn1 = OpenSSL::ASN1.decode(asn1timestamp)
|
||||
tstInfo = OpenSSL::ASN1.decode(asn1.value[1].value[0].value[2].value[1].value[0].value)
|
||||
tstInfo
|
||||
end
|
||||
|
||||
def self.signature_time(asn1timestamp)
|
||||
tstInfo = timestampInfo(asn1timestamp)
|
||||
tstInfo.value[4].value
|
||||
end
|
||||
|
||||
def self.signed_digest(asn1timestamp)
|
||||
tstInfo = timestampInfo(asn1timestamp)
|
||||
tstInfo.value[2].value[1].value.unpack1('H*')
|
||||
end
|
||||
end
|
40
app/lib/universign/api.rb
Normal file
40
app/lib/universign/api.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
class Universign::API
|
||||
## Universign Timestamp POST API
|
||||
# Official documentation is at https://help.universign.com/hc/fr/articles/360000898965-Guide-d-intégration-horodatage
|
||||
|
||||
def self.ensure_properly_configured!
|
||||
if userpwd.blank?
|
||||
raise StandardError, 'Universign API is not properly configured'
|
||||
end
|
||||
end
|
||||
|
||||
def self.timestamp(data)
|
||||
ensure_properly_configured!
|
||||
|
||||
response = Typhoeus.post(
|
||||
UNIVERSIGN_API_URL,
|
||||
userpwd: userpwd,
|
||||
body: body(data)
|
||||
)
|
||||
|
||||
if response.success?
|
||||
response.body
|
||||
else
|
||||
raise StandardError, "Universign timestamp query failed: #{response.status_message}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.body(data)
|
||||
{
|
||||
'hashAlgo': 'SHA256',
|
||||
'withCert': 'true',
|
||||
'hashValue': data
|
||||
}
|
||||
end
|
||||
|
||||
def self.userpwd
|
||||
Rails.application.secrets.universign[:userpwd]
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
class Avis < ApplicationRecord
|
||||
include EmailSanitizableConcern
|
||||
|
||||
belongs_to :dossier, touch: true
|
||||
belongs_to :dossier, inverse_of: :avis, touch: true
|
||||
belongs_to :gestionnaire
|
||||
belongs_to :claimant, class_name: 'Gestionnaire'
|
||||
|
||||
|
|
84
app/models/bill_signature.rb
Normal file
84
app/models/bill_signature.rb
Normal file
|
@ -0,0 +1,84 @@
|
|||
class BillSignature < ApplicationRecord
|
||||
has_many :dossier_operation_logs
|
||||
|
||||
has_one_attached :serialized
|
||||
has_one_attached :signature
|
||||
|
||||
validate :check_bill_digest
|
||||
validate :check_serialized_bill_contents
|
||||
validate :check_signature_contents
|
||||
|
||||
def self.build_with_operations(operations, day)
|
||||
bill = new(dossier_operation_logs: operations)
|
||||
|
||||
bill.serialize_operations(day)
|
||||
|
||||
bill
|
||||
end
|
||||
|
||||
def serialize_operations(day)
|
||||
self.serialized.attach(
|
||||
io: StringIO.new(operations_bill_json),
|
||||
filename: "demarches-simplifiees-operations-#{day.to_date.iso8601}.json",
|
||||
content_type: 'application/json',
|
||||
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
|
||||
)
|
||||
|
||||
self.digest = operations_bill_digest
|
||||
end
|
||||
|
||||
def operations_bill
|
||||
dossier_operation_logs.map { |op| [op.id.to_s, op.digest] }.to_h
|
||||
end
|
||||
|
||||
def operations_bill_json
|
||||
operations_bill.to_json
|
||||
end
|
||||
|
||||
def operations_bill_digest
|
||||
Digest::SHA256.hexdigest(operations_bill_json)
|
||||
end
|
||||
|
||||
def set_signature(signature, day)
|
||||
self.signature.attach(
|
||||
io: StringIO.new(signature),
|
||||
filename: "demarches-simplifiees-signature-#{day.to_date.iso8601}.der",
|
||||
content_type: 'application/x-x509-ca-cert'
|
||||
)
|
||||
end
|
||||
|
||||
# Validations
|
||||
def check_bill_digest
|
||||
if self.digest != self.operations_bill_digest
|
||||
errors.add(:digest)
|
||||
end
|
||||
end
|
||||
|
||||
def check_serialized_bill_contents
|
||||
if !self.serialized.attached?
|
||||
errors.add(:serialized, :blank)
|
||||
return
|
||||
end
|
||||
|
||||
if JSON.parse(self.serialized.download) != self.operations_bill
|
||||
errors.add(:serialized)
|
||||
end
|
||||
end
|
||||
|
||||
def check_signature_contents
|
||||
if !self.signature.attached?
|
||||
errors.add(:signature, :blank)
|
||||
return
|
||||
end
|
||||
|
||||
timestamp_signature_date = ASN1::Timestamp.signature_time(self.signature.download)
|
||||
if timestamp_signature_date > Time.zone.now
|
||||
errors.add(:signature, :invalid_date)
|
||||
end
|
||||
|
||||
timestamp_signed_digest = ASN1::Timestamp.signed_digest(self.signature.download)
|
||||
if timestamp_signed_digest != self.digest
|
||||
errors.add(:signature)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
class Champ < ApplicationRecord
|
||||
belongs_to :dossier, touch: true
|
||||
belongs_to :dossier, inverse_of: :champs, touch: true
|
||||
belongs_to :type_de_champ, inverse_of: :champ
|
||||
belongs_to :parent, class_name: 'Champ'
|
||||
has_many :commentaires
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Champs::RepetitionChamp < Champ
|
||||
has_many :champs, -> { ordered }, foreign_key: :parent_id, dependent: :destroy
|
||||
has_many :champs, -> { ordered }, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
|
||||
|
||||
accepts_nested_attributes_for :champs, allow_destroy: true
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Commentaire < ApplicationRecord
|
||||
belongs_to :dossier, touch: true
|
||||
belongs_to :dossier, inverse_of: :commentaires, touch: true
|
||||
belongs_to :piece_justificative
|
||||
|
||||
belongs_to :user
|
||||
|
|
|
@ -19,18 +19,18 @@ class Dossier < ApplicationRecord
|
|||
has_one :individual, dependent: :destroy
|
||||
has_one :attestation, dependent: :destroy
|
||||
|
||||
has_many :pieces_justificatives, dependent: :destroy
|
||||
has_many :pieces_justificatives, inverse_of: :dossier, dependent: :destroy
|
||||
has_one_attached :justificatif_motivation
|
||||
|
||||
has_many :champs, -> { root.public_only.ordered }, dependent: :destroy
|
||||
has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', dependent: :destroy
|
||||
has_many :commentaires, dependent: :destroy
|
||||
has_many :champs, -> { root.public_only.ordered }, inverse_of: :dossier, dependent: :destroy
|
||||
has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', inverse_of: :dossier, dependent: :destroy
|
||||
has_many :commentaires, inverse_of: :dossier, dependent: :destroy
|
||||
has_many :invites, dependent: :destroy
|
||||
has_many :follows, -> { active }
|
||||
has_many :previous_follows, -> { inactive }, class_name: 'Follow'
|
||||
has_many :follows, -> { active }, inverse_of: :dossier
|
||||
has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
|
||||
has_many :followers_gestionnaires, through: :follows, source: :gestionnaire
|
||||
has_many :previous_followers_gestionnaires, -> { distinct }, through: :previous_follows, source: :gestionnaire
|
||||
has_many :avis, dependent: :destroy
|
||||
has_many :avis, inverse_of: :dossier, dependent: :destroy
|
||||
|
||||
has_many :dossier_operation_logs, dependent: :destroy
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ class DossierOperationLog < ApplicationRecord
|
|||
|
||||
belongs_to :dossier
|
||||
has_one_attached :serialized
|
||||
belongs_to :bill_signature, optional: true
|
||||
|
||||
def self.create_and_serialize(params)
|
||||
dossier = params.fetch(:dossier)
|
||||
|
|
|
@ -12,12 +12,12 @@ class Gestionnaire < ApplicationRecord
|
|||
has_many :assign_to, dependent: :destroy
|
||||
has_many :procedures, through: :assign_to
|
||||
|
||||
has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo'
|
||||
has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo', inverse_of: :gestionnaire
|
||||
has_many :procedures_with_email_notifications, through: :assign_to_with_email_notifications, source: :procedure
|
||||
|
||||
has_many :dossiers, -> { state_not_brouillon }, through: :procedures
|
||||
has_many :follows, -> { active }
|
||||
has_many :previous_follows, -> { inactive }, class_name: 'Follow'
|
||||
has_many :follows, -> { active }, inverse_of: :gestionnaire
|
||||
has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :gestionnaire
|
||||
has_many :followed_dossiers, through: :follows, source: :dossier
|
||||
has_many :previously_followed_dossiers, -> { distinct }, through: :previous_follows, source: :dossier
|
||||
has_many :avis
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class PieceJustificative < ApplicationRecord
|
||||
belongs_to :dossier, touch: true
|
||||
belongs_to :dossier, inverse_of: :pieces_justificatives, touch: true
|
||||
belongs_to :type_de_piece_justificative
|
||||
has_one :commentaire
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ class Procedure < ApplicationRecord
|
|||
|
||||
MAX_DUREE_CONSERVATION = 36
|
||||
|
||||
has_many :types_de_piece_justificative, -> { ordered }, dependent: :destroy
|
||||
has_many :types_de_champ, -> { root.public_only.ordered }, dependent: :destroy
|
||||
has_many :types_de_champ_private, -> { root.private_only.ordered }, class_name: 'TypeDeChamp', dependent: :destroy
|
||||
has_many :types_de_piece_justificative, -> { ordered }, inverse_of: :procedure, dependent: :destroy
|
||||
has_many :types_de_champ, -> { root.public_only.ordered }, inverse_of: :procedure, dependent: :destroy
|
||||
has_many :types_de_champ_private, -> { root.private_only.ordered }, class_name: 'TypeDeChamp', inverse_of: :procedure, dependent: :destroy
|
||||
has_many :dossiers, dependent: :restrict_with_exception
|
||||
has_many :deleted_dossiers, dependent: :destroy
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class TypeDeChamp < ApplicationRecord
|
|||
belongs_to :procedure
|
||||
|
||||
belongs_to :parent, class_name: 'TypeDeChamp'
|
||||
has_many :types_de_champ, -> { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', dependent: :destroy
|
||||
has_many :types_de_champ, -> { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', inverse_of: :parent, dependent: :destroy
|
||||
|
||||
store_accessor :options, :cadastres, :quartiers_prioritaires, :parcelles_agricoles, :old_pj
|
||||
delegate :tags_for_template, to: :dynamic_type
|
||||
|
|
16
app/services/bill_signature_service.rb
Normal file
16
app/services/bill_signature_service.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class BillSignatureService
|
||||
def self.grouped_unsigned_operation_until(date)
|
||||
unsigned_operations = DossierOperationLog
|
||||
.where(bill_signature: nil)
|
||||
.where('executed_at < ?', date)
|
||||
|
||||
unsigned_operations.group_by { |e| e.executed_at.to_date }
|
||||
end
|
||||
|
||||
def self.sign_operations(operations, day)
|
||||
bill = BillSignature.build_with_operations(operations, day)
|
||||
signature = Universign::API.timestamp(bill.digest)
|
||||
bill.set_signature(signature, day)
|
||||
bill.save!
|
||||
end
|
||||
end
|
1
app/views/fields/attachment_field/_index.html.haml
Normal file
1
app/views/fields/attachment_field/_index.html.haml
Normal file
|
@ -0,0 +1 @@
|
|||
= link_to(field.to_s, field.blob_path)
|
68
app/views/manager/bill_signatures/_collection.html.erb
Normal file
68
app/views/manager/bill_signatures/_collection.html.erb
Normal file
|
@ -0,0 +1,68 @@
|
|||
<%#
|
||||
# Collection
|
||||
|
||||
This partial is used on the `index` and `show` pages
|
||||
to display a collection of resources in an HTML table.
|
||||
|
||||
## Local variables:
|
||||
|
||||
- `collection_presenter`:
|
||||
An instance of [Administrate::Page::Collection][1].
|
||||
The table presenter uses `ResourceDashboard::COLLECTION_ATTRIBUTES` to determine
|
||||
the columns displayed in the table
|
||||
- `resources`:
|
||||
An ActiveModel::Relation collection of resources to be displayed in the table.
|
||||
By default, the number of resources is limited by pagination
|
||||
or by a hard limit to prevent excessive page load times
|
||||
|
||||
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
|
||||
%>
|
||||
|
||||
<table aria-labelledby="<%= table_title %>">
|
||||
<thead>
|
||||
<tr>
|
||||
<% collection_presenter.attribute_types.each do |attr_name, attr_type| %>
|
||||
<th class="cell-label
|
||||
cell-label--<%= attr_type.html_class %>
|
||||
cell-label--<%= collection_presenter.ordered_html_class(attr_name) %>"
|
||||
scope="col"
|
||||
role="columnheader"
|
||||
aria-sort="<%= sort_order(collection_presenter.ordered_html_class(attr_name)) %>">
|
||||
<%= link_to(sanitized_order_params(page, collection_field_name).merge(
|
||||
collection_presenter.order_params_for(attr_name, key: collection_field_name)
|
||||
)) do %>
|
||||
<%= t(
|
||||
"helpers.label.#{collection_presenter.resource_name}.#{attr_name}",
|
||||
default: attr_name.to_s,
|
||||
).titleize %>
|
||||
<% if collection_presenter.ordered_by?(attr_name) %>
|
||||
<span class="cell-label__sort-indicator cell-label__sort-indicator--<%= collection_presenter.ordered_html_class(attr_name) %>">
|
||||
<svg aria-hidden="true">
|
||||
<use xlink:href="#icon-up-caret" />
|
||||
</svg>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</th>
|
||||
<% end %>
|
||||
<% [valid_action?(:edit, collection_presenter.resource_name),
|
||||
valid_action?(:destroy, collection_presenter.resource_name)].count(true).times do %>
|
||||
<th scope="col"></th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% resources.each do |resource| %>
|
||||
<tr class="js-table-row"
|
||||
tabindex="0"
|
||||
>
|
||||
<% collection_presenter.attributes_for(resource).each do |attribute| %>
|
||||
<td class="cell-data cell-data--<%= attribute.html_class %>">
|
||||
<%= render_field attribute %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
|
@ -66,3 +66,6 @@ TRUSTED_NETWORKS=""
|
|||
SKYLIGHT_AUTHENTICATION_KEY=""
|
||||
|
||||
LOGRAGE_ENABLED="disabled"
|
||||
|
||||
UNIVERSIGN_API_URL=""
|
||||
UNIVERSIGN_USERPWD=""
|
||||
|
|
|
@ -7,6 +7,7 @@ API_GEO_SANDBOX_URL = ENV.fetch("API_GEO_SANDBOX_URL", "https://sandbox.geo.api.
|
|||
HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2")
|
||||
PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1")
|
||||
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")
|
||||
UNIVERSIGN_API_URL = ENV.fetch("UNIVERSIGN_API_URL", "https://ws.universign.eu/tsa/post/")
|
||||
|
||||
# Internal URLs
|
||||
FOG_BASE_URL = "https://static.demarches-simplifiees.fr"
|
||||
|
|
34
config/locales/models/bill_signature/fr.yml
Normal file
34
config/locales/models/bill_signature/fr.yml
Normal file
|
@ -0,0 +1,34 @@
|
|||
fr:
|
||||
activerecord:
|
||||
attributes:
|
||||
bill_signature:
|
||||
dossier_operation_logs:
|
||||
one: opération
|
||||
other: opérations
|
||||
digest: empreinte
|
||||
serialized: liasse
|
||||
signature: signature
|
||||
errors:
|
||||
models:
|
||||
bill_signature:
|
||||
attributes:
|
||||
digest:
|
||||
invalid: 'ne correspond pas à la liasse'
|
||||
serialized:
|
||||
blank: 'doit être rempli'
|
||||
invalid: 'ne correspond pas aux opérations'
|
||||
signature:
|
||||
blank: 'doit être rempli'
|
||||
invalid: 'ne correspond pas à l’empreinte'
|
||||
invalid_date: 'ne doit pas être dans le futur'
|
||||
models:
|
||||
bill_signature:
|
||||
one: Horodatage
|
||||
other: Horodatages
|
||||
helpers:
|
||||
label:
|
||||
bill_signature:
|
||||
dossier_operation_logs: opérations
|
||||
digest: empreinte
|
||||
serialized: liasse
|
||||
signature: signature
|
|
@ -37,6 +37,8 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :demandes, only: [:index]
|
||||
|
||||
resources :bill_signatures, only: [:index]
|
||||
|
||||
resources :services, only: [:index, :show]
|
||||
|
||||
post 'demandes/create_administrateur'
|
||||
|
|
|
@ -64,6 +64,8 @@ defaults: &defaults
|
|||
crisp:
|
||||
enabled: <%= ENV['CRISP_ENABLED'] == 'enabled' %>
|
||||
client_key: <%= ENV['CRISP_CLIENT_KEY'] %>
|
||||
universign:
|
||||
userpwd: <%= ENV['UNIVERSIGN_USERPWD'] %>
|
||||
|
||||
|
||||
|
||||
|
@ -90,6 +92,8 @@ test:
|
|||
token_endpoint: https://bidon.com/endpoint
|
||||
userinfo_endpoint: https://bidon.com/endpoint
|
||||
logout_endpoint: https://bidon.com/endpoint
|
||||
universign:
|
||||
userpwd: 'fake:fake'
|
||||
|
||||
# Do not keep production secrets in the repository,
|
||||
# instead read values from the environment.
|
||||
|
|
10
db/migrate/20190616141702_create_bill_signature.rb
Normal file
10
db/migrate/20190616141702_create_bill_signature.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
class CreateBillSignature < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :bill_signatures do |t|
|
||||
t.string :digest
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_reference :dossier_operation_logs, :bill_signature, foreign_key: true
|
||||
end
|
||||
end
|
11
db/schema.rb
11
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2019_06_07_124156) do
|
||||
ActiveRecord::Schema.define(version: 2019_06_16_141702) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -145,6 +145,12 @@ ActiveRecord::Schema.define(version: 2019_06_07_124156) do
|
|||
t.index ["gestionnaire_id"], name: "index_avis_on_gestionnaire_id"
|
||||
end
|
||||
|
||||
create_table "bill_signatures", force: :cascade do |t|
|
||||
t.string "digest"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "champs", id: :serial, force: :cascade do |t|
|
||||
t.string "value"
|
||||
t.integer "type_de_champ_id"
|
||||
|
@ -224,7 +230,9 @@ ActiveRecord::Schema.define(version: 2019_06_07_124156) do
|
|||
t.datetime "keep_until"
|
||||
t.datetime "executed_at"
|
||||
t.text "digest"
|
||||
t.bigint "bill_signature_id"
|
||||
t.index ["administration_id"], name: "index_dossier_operation_logs_on_administration_id"
|
||||
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 ["gestionnaire_id"], name: "index_dossier_operation_logs_on_gestionnaire_id"
|
||||
t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until"
|
||||
|
@ -617,6 +625,7 @@ ActiveRecord::Schema.define(version: 2019_06_07_124156) do
|
|||
add_foreign_key "closed_mails", "procedures"
|
||||
add_foreign_key "commentaires", "dossiers"
|
||||
add_foreign_key "dossier_operation_logs", "administrations"
|
||||
add_foreign_key "dossier_operation_logs", "bill_signatures"
|
||||
add_foreign_key "dossier_operation_logs", "dossiers"
|
||||
add_foreign_key "dossier_operation_logs", "gestionnaires"
|
||||
add_foreign_key "dossiers", "users"
|
||||
|
|
|
@ -386,9 +386,11 @@ describe Users::DossiersController, type: :controller do
|
|||
|
||||
describe '#update_brouillon' do
|
||||
before { sign_in(user) }
|
||||
|
||||
let!(:dossier) { create(:dossier, user: user) }
|
||||
let(:first_champ) { dossier.champs.first }
|
||||
let(:value) { 'beautiful value' }
|
||||
let(:now) { Time.zone.parse('01/01/2100') }
|
||||
let(:submit_payload) do
|
||||
{
|
||||
id: dossier.id,
|
||||
|
@ -402,7 +404,11 @@ describe Users::DossiersController, type: :controller do
|
|||
end
|
||||
let(:payload) { submit_payload }
|
||||
|
||||
subject { patch :update_brouillon, params: payload }
|
||||
subject do
|
||||
Timecop.freeze(now) do
|
||||
patch :update_brouillon, params: payload
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the dossier cannot be updated by the user' do
|
||||
let!(:dossier) { create(:dossier, :en_instruction, user: user) }
|
||||
|
@ -421,6 +427,7 @@ describe Users::DossiersController, type: :controller do
|
|||
|
||||
expect(response).to redirect_to(merci_dossier_path(dossier))
|
||||
expect(first_champ.reload.value).to eq('beautiful value')
|
||||
expect(dossier.reload.updated_at.year).to eq(2100)
|
||||
expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_construction))
|
||||
end
|
||||
|
||||
|
@ -549,9 +556,15 @@ describe Users::DossiersController, type: :controller do
|
|||
|
||||
describe '#update' do
|
||||
before { sign_in(user) }
|
||||
let!(:dossier) { create(:dossier, :en_construction, user: user) }
|
||||
|
||||
let(:procedure) { create(:procedure, :published, :with_type_de_champ, :with_piece_justificative) }
|
||||
let!(:dossier) { create(:dossier, :en_construction, user: user, procedure: procedure) }
|
||||
let(:first_champ) { dossier.champs.first }
|
||||
let(:piece_justificative_champ) { dossier.champs.last }
|
||||
let(:value) { 'beautiful value' }
|
||||
let(:file) { Rack::Test::UploadedFile.new("./spec/fixtures/files/piece_justificative_0.pdf", 'application/pdf') }
|
||||
let(:now) { Time.zone.parse('01/01/2100') }
|
||||
|
||||
let(:submit_payload) do
|
||||
{
|
||||
id: dossier.id,
|
||||
|
@ -565,7 +578,11 @@ describe Users::DossiersController, type: :controller do
|
|||
end
|
||||
let(:payload) { submit_payload }
|
||||
|
||||
subject { patch :update, params: payload }
|
||||
subject do
|
||||
Timecop.freeze(now) do
|
||||
patch :update, params: payload
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the dossier cannot be updated by the user' do
|
||||
let!(:dossier) { create(:dossier, :en_instruction, user: user) }
|
||||
|
@ -584,8 +601,28 @@ describe Users::DossiersController, type: :controller do
|
|||
|
||||
expect(response).to redirect_to(demande_dossier_path(dossier))
|
||||
expect(first_champ.reload.value).to eq('beautiful value')
|
||||
expect(dossier.reload.updated_at.year).to eq(2100)
|
||||
expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_construction))
|
||||
end
|
||||
|
||||
context 'when only files champs are modified' do
|
||||
let(:submit_payload) do
|
||||
{
|
||||
id: dossier.id,
|
||||
dossier: {
|
||||
champs_attributes: {
|
||||
id: piece_justificative_champ.id,
|
||||
piece_justificative_file: file
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates the dossier modification date' do
|
||||
subject
|
||||
expect(dossier.reload.updated_at.year).to eq(2100)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the update fails' do
|
||||
|
|
6
spec/factories/bill_signature.rb
Normal file
6
spec/factories/bill_signature.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
FactoryBot.define do
|
||||
factory :bill_signature do
|
||||
serialized { Rack::Test::UploadedFile.new("./spec/fixtures/files/bill_signature/serialized.json", 'application/json') }
|
||||
signature { Rack::Test::UploadedFile.new("./spec/fixtures/files/bill_signature/signature.der", 'application/x-x509-ca-cert') }
|
||||
end
|
||||
end
|
5
spec/factories/dossier_operation_log.rb
Normal file
5
spec/factories/dossier_operation_log.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
FactoryBot.define do
|
||||
factory :dossier_operation_log do
|
||||
operation { :passer_en_instruction }
|
||||
end
|
||||
end
|
|
@ -127,6 +127,14 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :with_piece_justificative do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
type_de_champ = create(:type_de_champ_piece_justificative)
|
||||
procedure.types_de_champ << type_de_champ
|
||||
end
|
||||
end
|
||||
|
||||
# Deprecated
|
||||
trait :with_two_type_de_piece_justificative do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
rib = create(:type_de_piece_justificative, :rib, order_place: 1)
|
||||
|
|
43
spec/fixtures/cassettes/universign.yml
vendored
Normal file
43
spec/fixtures/cassettes/universign.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://ws.universign.eu/tsa/post/
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: hashAlgo=SHA256&hashValue=d28d6c7742c6c77025a104b81750ce8dc5f7dba2d01d9b5f4cad828f02b2324b&withCert=true
|
||||
headers:
|
||||
User-Agent:
|
||||
- demarches-simplifiees.fr
|
||||
Expect:
|
||||
- ''
|
||||
Authorization:
|
||||
- Basic cmllbiBpY2kK=
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Fri, 17 May 2019 10:05:12 GMT
|
||||
Server:
|
||||
- Apache
|
||||
Cache-Control:
|
||||
- no-cache
|
||||
Pragma:
|
||||
- no-cache
|
||||
Content-Type:
|
||||
- application/octet-stream
|
||||
Set-Cookie:
|
||||
- CGSESSIONID=A3D72C1B0D07C073CFE68597B3FBF1B6E639C351;Path=/;Version=0
|
||||
X-Ua-Compatible:
|
||||
- IE=Edge
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: !binary |-
|
||||
MIIHvAYJKoZIhvcNAQcCoIIHrTCCB6kCAQMxDzANBglghkgBZQMEAgEFADCBxwYLKoZIhvcNAQkQAQSggbcEgbQwgbECAQEGBCoDBAUwMTANBglghkgBZQMEAgEFAAQg0o1sd0LGx3AloQS4F1DOjcX326LQHZtfTK2CjwKyMksCFQCGAZpWDFVXaMOe1wHG3iVRUofhGxgTMjAxOTA1MTcxMDA1MTIuNTUyWjADgAEBAQH/oD+kPTA7MQswCQYDVQQGEwJGUjESMBAGA1UEChMJQ3J5cHRvbG9nMRgwFgYDVQQDEw9UZXN0IFRTQSAtIFRVIDWgggReMIIEWjCCA0KgAwIBAgIUdG+qwQPj0VPrIVMSA2dAelI+Cf8wDQYJKoZIhvcNAQELBQAwQzELMAkGA1UEBhMCRlIxEjAQBgNVBAoTCUNyeXB0b2xvZzEgMB4GA1UEAxMXVGVzdCBVbml2ZXJzaWduIENBIDIwMTgwHhcNMTgxMjA0MjMwMDAwWhcNMjUxMTMwMjMwMDAwWjA7MQswCQYDVQQGEwJGUjESMBAGA1UEChMJQ3J5cHRvbG9nMRgwFgYDVQQDEw9UZXN0IFRTQSAtIFRVIDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpgkQuUvnZC5a+EbND79BnJuNypMJA0NLKJCqERBKi13WqXid5w7HNKkoIjQtYmxNhfx9b5PeVvTuaGFgMNajAqxwOwA78bqsKhyN/UNL1WYCZSMXiy2TTKvSNs4Ea50Ymu3lkOm/223d1KJhtjT69AlZz18OIXCuIvROr1vPfNvww2GE3RSpV9ro+Ip09oq2KQ5ylAxBSdt4ZoTQvZDDvHxELbjBOEJBe2T9f3KdZR+irl3sRScDtHTPE0lT58c3iKBBqdwFJSYE/KpSW0lD8HCMjZui4jNnHD6WcTo+KASPYxB9OSpM2KJ0NyCp7PrSQIL6o8H5c6nSQLUK+DMQbAgMBAAGjggFMMIIBSDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFInD+siND0GFha6Rko94dOCVD8cdMIGTBggrBgEFBQcBAQSBhjCBgzAzBggrBgEFBQcwAYYnaHR0cDovL3BraS1yZWNldHRlLnVuaXZlcnNpZ24uY29tOjgwMjIvMEwGCCsGAQUFBzAChkBodHRwczovL3BraS1yZWNldHRlLnVuaXZlcnNpZ24uY29tL2NlcnRpZmljYXRlL3VuaXZlcnNpZ24tY2EuY2VyMEgGA1UdHwRBMD8wPaA7oDmGN2h0dHA6Ly9wa2ktcmVjZXR0ZS51bml2ZXJzaWduLmNvbS9jcmwvdW5pdmVyc2lnbi1jYS5jcmwwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwHwYDVR0jBBgwFoAUUJIhO5u6g1uzzgr2ksBY7IzFBHkwDQYJKoZIhvcNAQELBQADggEBAFZNY26oPFCca9Yv/fohsDPvhgXt7Ko+v6eLVMo1AtK2kC5XfWFp69Tv3QyJNLl/Z8wRtKQv3/g7eGkJFQDmD2n5g2CJABaYxG/4c8bN/lYQR27VVVRENaAxwbjbYo04mQ50asZgFfefrYydp59cbvDji74+jywjHc+kFv7Ty8JpAgVVMhjZ+8SyXqzc1ODiCz9R5ZBx8BKZbmixW15N/21NyMF/e0J7hJrS1CGkqSZ/MPIAXqJhFaqeeuk/m0pya2F1DG+tCQF25mZj5NMwqRrdH/jsoLnSmV70WT7kwTIxDFb+a1ec9TWAH/8t87waYBV2hIZe/piNB0szOcTRXwgxggJlMIICYQIBATBbMEMxCzAJBgNVBAYTAkZSMRIwEAYDVQQKEwlDcnlwdG9sb2cxIDAeBgNVBAMTF1Rlc3QgVW5pdmVyc2lnbiBDQSAyMDE4AhR0b6rBA+PRU+shUxIDZ0B6Uj4J/zANBglghkgBZQMEAgEFAKCB3DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIKYzfvcT1DFcu5RhtfRnC3938fFuOuGKbMtkj0ihg0eJMIGMBgsqhkiG9w0BCRACDDF9MHsweTB3BBSo55pEUwOP+3HqXuFzE96z9mmjsjBfMEekRTBDMQswCQYDVQQGEwJGUjESMBAGA1UEChMJQ3J5cHRvbG9nMSAwHgYDVQQDExdUZXN0IFVuaXZlcnNpZ24gQ0EgMjAxOAIUdG+qwQPj0VPrIVMSA2dAelI+Cf8wDQYJKoZIhvcNAQELBQAEggEATud6KAo3mPxhk4OpV1eNKAzml2uC7TAjlxrWAS1wRZgSJD35zt3/K9NoDLGxT+k9POLmxqEi8AGE4PZxArveTmTBe2QHQEk3jVsRuQFbu3jzf7Q0iGlUrJVK8Y4ndhGFPxhUdUKSjMHoG7nxhLD2OddcRZf7TtAuizhfYNGyFB8zwKleVxA1z61Jb6GjdKEGTV4H09d7xKVz6ov43Q6277STqTSgQ8VWlvC2pnhDjwq1D+Ehex9//PU2Qa9XHJnjz68E+BR8yyMWkipOHbqmjNflkoeXb2hVo8bpaA9oMmJLvHMvq7O65LESIZKrVYIbUiMKn1G5E4muyafMTFfdpQ==
|
||||
http_version:
|
||||
recorded_at: Fri, 17 May 2019 10:05:12 GMT
|
||||
recorded_with: VCR 4.0.0
|
1
spec/fixtures/files/bill_signature/serialized.json
vendored
Normal file
1
spec/fixtures/files/bill_signature/serialized.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"dossier1": "hash1", "dossier2": "hash2"}
|
BIN
spec/fixtures/files/bill_signature/signature.der
vendored
Normal file
BIN
spec/fixtures/files/bill_signature/signature.der
vendored
Normal file
Binary file not shown.
19
spec/lib/asn1/timestamp_spec.rb
Normal file
19
spec/lib/asn1/timestamp_spec.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ASN1::Timestamp do
|
||||
let(:asn1timestamp) { File.read('spec/fixtures/files/bill_signature/signature.der') }
|
||||
|
||||
describe '.timestamp_time' do
|
||||
subject { described_class.signature_time(asn1timestamp) }
|
||||
|
||||
it { is_expected.to eq Time.zone.parse('2019-04-30 15:30:20 UTC') }
|
||||
end
|
||||
|
||||
describe '.timestamp_signed_data' do
|
||||
subject { described_class.signed_digest(asn1timestamp) }
|
||||
|
||||
let(:data) { Digest::SHA256.hexdigest('CECI EST UN BLOB') }
|
||||
|
||||
it { is_expected.to eq data }
|
||||
end
|
||||
end
|
11
spec/lib/universign/api_spec.rb
Normal file
11
spec/lib/universign/api_spec.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Universign::API do
|
||||
describe '.request_timestamp', vcr: { cassette_name: 'universign' } do
|
||||
subject { described_class.timestamp(digest) }
|
||||
|
||||
let(:digest) { Digest::SHA256.hexdigest("CECI EST UN HASH") }
|
||||
|
||||
it { is_expected.not_to be_nil }
|
||||
end
|
||||
end
|
160
spec/models/bill_signature_spec.rb
Normal file
160
spec/models/bill_signature_spec.rb
Normal file
|
@ -0,0 +1,160 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe BillSignature, type: :model do
|
||||
describe 'validations' do
|
||||
subject(:bill_signature) { BillSignature.new }
|
||||
|
||||
describe 'check_bill_digest' do
|
||||
before do
|
||||
bill_signature.dossier_operation_logs = dossier_operation_logs
|
||||
bill_signature.digest = digest
|
||||
bill_signature.valid?
|
||||
end
|
||||
|
||||
context 'when there are no operations' do
|
||||
let(:dossier_operation_logs) { [] }
|
||||
|
||||
context 'when the digest is correct' do
|
||||
let(:digest) { Digest::SHA256.hexdigest('{}') }
|
||||
|
||||
it { expect(bill_signature.errors.details[:digest]).to be_empty }
|
||||
end
|
||||
|
||||
context 'when the digest is incorrect' do
|
||||
let(:digest) { 'baadf00d' }
|
||||
|
||||
it { expect(bill_signature.errors.details[:digest]).to eq [error: :invalid] }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the signature has operations' do
|
||||
let(:dossier_operation_logs) { [build(:dossier_operation_log, id: '1234', digest: 'abcd')] }
|
||||
|
||||
context 'when the digest is correct' do
|
||||
let(:digest) { Digest::SHA256.hexdigest('{"1234":"abcd"}') }
|
||||
|
||||
it { expect(bill_signature.errors.details[:digest]).to be_empty }
|
||||
end
|
||||
|
||||
context 'when the digest is incorrect' do
|
||||
let(:digest) { 'baadf00d' }
|
||||
|
||||
it { expect(bill_signature.errors.details[:digest]).to eq [error: :invalid] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'check_serialized_bill_contents' do
|
||||
before do
|
||||
bill_signature.dossier_operation_logs = dossier_operation_logs
|
||||
bill_signature.serialized.attach(io: StringIO.new(serialized), filename: 'file') if serialized.present?
|
||||
bill_signature.valid?
|
||||
end
|
||||
|
||||
context 'when there are no operations' do
|
||||
let(:dossier_operation_logs) { [] }
|
||||
let(:serialized) { '{}' }
|
||||
|
||||
it { expect(bill_signature.errors.details[:serialized]).to be_empty }
|
||||
end
|
||||
|
||||
context 'when the signature has operations' do
|
||||
let(:dossier_operation_logs) { [build(:dossier_operation_log, id: '1234', digest: 'abcd')] }
|
||||
let(:serialized) { '{"1234":"abcd"}' }
|
||||
|
||||
it { expect(bill_signature.errors.details[:serialized]).to be_empty }
|
||||
end
|
||||
|
||||
context 'when serialized isn’t set' do
|
||||
let(:dossier_operation_logs) { [] }
|
||||
let(:serialized) { nil }
|
||||
|
||||
it { expect(bill_signature.errors.details[:serialized]).to eq [error: :blank] }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'check_signature_contents' do
|
||||
before do
|
||||
bill_signature.signature.attach(io: StringIO.new(signature), filename: 'file') if signature.present?
|
||||
allow(ASN1::Timestamp).to receive(:signature_time).and_return(signature_time)
|
||||
allow(ASN1::Timestamp).to receive(:signed_digest).and_return(signed_digest)
|
||||
bill_signature.digest = digest
|
||||
bill_signature.valid?
|
||||
end
|
||||
|
||||
context 'when the signature is correct' do
|
||||
let(:signature) { 'signature' }
|
||||
let(:signature_time) { 1.day.ago }
|
||||
let(:digest) { 'abcd' }
|
||||
let(:signed_digest) { 'abcd' }
|
||||
|
||||
it { expect(bill_signature.errors.details[:signature]).to be_empty }
|
||||
end
|
||||
|
||||
context 'when the signature isn’t set' do
|
||||
let(:signature) { nil }
|
||||
let(:signature_time) { 1.day.ago }
|
||||
let(:digest) { 'abcd' }
|
||||
let(:signed_digest) { 'abcd' }
|
||||
|
||||
it { expect(bill_signature.errors.details[:signature]).to eq [error: :blank] }
|
||||
end
|
||||
|
||||
context 'when the signature time is in the future' do
|
||||
let(:signature) { 'signature' }
|
||||
let(:signature_time) { 1.day.from_now }
|
||||
let(:digest) { 'abcd' }
|
||||
let(:signed_digest) { 'abcd' }
|
||||
|
||||
it { expect(bill_signature.errors.details[:signature]).to eq [error: :invalid_date] }
|
||||
end
|
||||
|
||||
context 'when the signature doesn’t match the digest' do
|
||||
let(:signature) { 'signature' }
|
||||
let(:signature_time) { 1.day.ago }
|
||||
let(:digest) { 'abcd' }
|
||||
let(:signed_digest) { 'dcba' }
|
||||
|
||||
it { expect(bill_signature.errors.details[:signature]).to eq [error: :invalid] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.build_with_operations' do
|
||||
subject(:bill_signature) { described_class.build_with_operations(dossier_operation_logs, Date.new(1871, 03, 18)) }
|
||||
|
||||
context 'when there are no operations' do
|
||||
let(:dossier_operation_logs) { [] }
|
||||
|
||||
it { expect(bill_signature.operations_bill).to eq({}) }
|
||||
it { expect(bill_signature.digest).to eq(Digest::SHA256.hexdigest('{}')) }
|
||||
it { expect(bill_signature.serialized.download).to eq('{}') }
|
||||
it { expect(bill_signature.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') }
|
||||
end
|
||||
|
||||
context 'when there is one operation' do
|
||||
let(:dossier_operation_logs) do
|
||||
[build(:dossier_operation_log, id: '1234', digest: 'abcd')]
|
||||
end
|
||||
|
||||
it { expect(bill_signature.operations_bill).to eq({ '1234' => 'abcd' }) }
|
||||
it { expect(bill_signature.digest).to eq(Digest::SHA256.hexdigest('{"1234":"abcd"}')) }
|
||||
it { expect(bill_signature.serialized.download).to eq('{"1234":"abcd"}') }
|
||||
it { expect(bill_signature.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') }
|
||||
end
|
||||
|
||||
context 'when there are several operations' do
|
||||
let(:dossier_operation_logs) do
|
||||
[
|
||||
build(:dossier_operation_log, id: '1234', digest: 'abcd'),
|
||||
build(:dossier_operation_log, id: '5678', digest: 'dcba')
|
||||
]
|
||||
end
|
||||
|
||||
it { expect(bill_signature.operations_bill).to eq({ '1234' => 'abcd', '5678' => 'dcba' }) }
|
||||
it { expect(bill_signature.digest).to eq(Digest::SHA256.hexdigest('{"1234":"abcd","5678":"dcba"}')) }
|
||||
it { expect(bill_signature.serialized.download).to eq('{"1234":"abcd","5678":"dcba"}') }
|
||||
it { expect(bill_signature.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') }
|
||||
end
|
||||
end
|
||||
end
|
37
spec/services/bill_signature_service_spec.rb
Normal file
37
spec/services/bill_signature_service_spec.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe BillSignatureService do
|
||||
describe ".grouped_unsigned_operation_until" do
|
||||
subject { BillSignatureService.grouped_unsigned_operation_until(date).length }
|
||||
|
||||
let(:date) { Date.today }
|
||||
|
||||
context "when operations of several days need to be signed" do
|
||||
before do
|
||||
create :dossier_operation_log, executed_at: 3.days.ago
|
||||
create :dossier_operation_log, executed_at: 2.days.ago
|
||||
create :dossier_operation_log, executed_at: 1.day.ago
|
||||
end
|
||||
|
||||
it { is_expected.to eq 3 }
|
||||
end
|
||||
|
||||
context "when operations on a single day need to be signed" do
|
||||
before do
|
||||
create :dossier_operation_log, executed_at: 1.day.ago
|
||||
create :dossier_operation_log, executed_at: 1.day.ago
|
||||
end
|
||||
|
||||
it { is_expected.to eq 1 }
|
||||
end
|
||||
|
||||
context "when there are no operations to be signed" do
|
||||
before do
|
||||
create :dossier_operation_log, created_at: 1.day.ago, bill_signature: build(:bill_signature)
|
||||
create :dossier_operation_log, created_at: 1.day.from_now
|
||||
end
|
||||
|
||||
it { is_expected.to eq 0 }
|
||||
end
|
||||
end
|
||||
end
|
40
yarn.lock
40
yarn.lock
|
@ -3533,9 +3533,9 @@ fs.realpath@^1.0.0:
|
|||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
fsevents@^1.2.7:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.8.tgz#57ea5320f762cd4696e5e8e87120eccc8b11cacf"
|
||||
integrity sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA==
|
||||
version "1.2.9"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f"
|
||||
integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==
|
||||
dependencies:
|
||||
nan "^2.12.1"
|
||||
node-pre-gyp "^0.12.0"
|
||||
|
@ -4788,16 +4788,6 @@ lodash._reinterpolate@~3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
|
||||
|
||||
lodash.assign@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
|
||||
integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=
|
||||
|
||||
lodash.clonedeep@^4.3.2:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
|
||||
|
||||
lodash.get@^4.0, lodash.get@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
|
@ -4813,11 +4803,6 @@ lodash.memoize@^4.1.2:
|
|||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
|
||||
|
||||
lodash.mergewith@^4.6.0:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
|
||||
integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
|
||||
|
||||
lodash.tail@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
|
||||
|
@ -5197,11 +5182,16 @@ mute-stream@0.0.7:
|
|||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
||||
|
||||
nan@^2.10.0, nan@^2.12.1:
|
||||
nan@^2.12.1:
|
||||
version "2.13.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
|
||||
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
|
||||
|
||||
nan@^2.13.2:
|
||||
version "2.14.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
||||
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||
|
@ -5324,9 +5314,9 @@ node-releases@^1.1.14:
|
|||
semver "^5.3.0"
|
||||
|
||||
node-sass@^4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a"
|
||||
integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017"
|
||||
integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==
|
||||
dependencies:
|
||||
async-foreach "^0.1.3"
|
||||
chalk "^1.1.1"
|
||||
|
@ -5335,12 +5325,10 @@ node-sass@^4.11.0:
|
|||
get-stdin "^4.0.1"
|
||||
glob "^7.0.3"
|
||||
in-publish "^2.0.0"
|
||||
lodash.assign "^4.2.0"
|
||||
lodash.clonedeep "^4.3.2"
|
||||
lodash.mergewith "^4.6.0"
|
||||
lodash "^4.17.11"
|
||||
meow "^3.7.0"
|
||||
mkdirp "^0.5.1"
|
||||
nan "^2.10.0"
|
||||
nan "^2.13.2"
|
||||
node-gyp "^3.8.0"
|
||||
npmlog "^4.0.0"
|
||||
request "^2.88.0"
|
||||
|
|
Loading…
Reference in a new issue