Merge pull request #3965 from betagouv/dev

2019-06-17-02
This commit is contained in:
Nicolas Bouilleaud 2019-06-17 17:32:50 +02:00 committed by GitHub
commit 9304c4faf9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 719 additions and 55 deletions

View file

@ -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

View file

@ -726,7 +726,7 @@ Rails/HttpStatus:
Enabled: false
Rails/InverseOf:
Enabled: false
Enabled: true
Rails/LexicallyScopedActionFilter:
Enabled: false

View file

@ -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 dhéberger une autre instance vous-même**.
Dans le cas où vous envisagez dhé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 dhéberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni dassurer de support technique concernant votre installation.
Dans le cas où vous envisagez dhé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, ladministration 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 dun retour dexpérience, et de bénéficier de leur retour.

View file

@ -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

View file

@ -0,0 +1,4 @@
module Manager
class BillSignaturesController < Manager::ApplicationController
end
end

View 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

View 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

View 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
View file

@ -0,0 +1,25 @@
class ASN1::Timestamp
## Poor mans 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:
# * Its hard to do right.
# * We currently dont require it for proper operation; timestamps are never exposed to users.
# * Theres an ongoing PR https://github.com/ruby/openssl/pull/204 for proper timestamp decoding in the ruby openssl library; lets 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
View 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

View file

@ -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'

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -0,0 +1 @@
= link_to(field.to_s, field.blob_path)

View 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>

View file

@ -66,3 +66,6 @@ TRUSTED_NETWORKS=""
SKYLIGHT_AUTHENTICATION_KEY=""
LOGRAGE_ENABLED="disabled"
UNIVERSIGN_API_URL=""
UNIVERSIGN_USERPWD=""

View file

@ -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"

View 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 à lempreinte'
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

View file

@ -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'

View file

@ -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.

View 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

View file

@ -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"

View file

@ -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

View 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

View file

@ -0,0 +1,5 @@
FactoryBot.define do
factory :dossier_operation_log do
operation { :passer_en_instruction }
end
end

View file

@ -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
View 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 |-
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
http_version:
recorded_at: Fri, 17 May 2019 10:05:12 GMT
recorded_with: VCR 4.0.0

View file

@ -0,0 +1 @@
{"dossier1": "hash1", "dossier2": "hash2"}

Binary file not shown.

View 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

View 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

View 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 isnt 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 isnt 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 doesnt 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

View 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

View file

@ -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"