Merge pull request #3886 from betagouv/timestamp-dossier-operations

Timestamp operations
This commit is contained in:
Nicolas Bouilleaud 2019-06-17 16:38:47 +02:00 committed by GitHub
commit de79a9a8bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 635 additions and 2 deletions

View file

@ -10,5 +10,5 @@ indent_size = 2
indent_style = space indent_style = space
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.{ico,keep,pdf,svg}] [*.{ico,keep,pdf,svg,der}]
insert_final_newline = false insert_final_newline = false

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 WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later
GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later
PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later
OperationsSignatureJob.set(cron: "0 6 * * *").perform_later
### Voir les emails envoyés en local ### 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

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

@ -12,6 +12,7 @@ class DossierOperationLog < ApplicationRecord
belongs_to :dossier belongs_to :dossier
has_one_attached :serialized has_one_attached :serialized
belongs_to :bill_signature, optional: true
def self.create_and_serialize(params) def self.create_and_serialize(params)
dossier = params.fetch(:dossier) dossier = params.fetch(:dossier)

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="" SKYLIGHT_AUTHENTICATION_KEY=""
LOGRAGE_ENABLED="disabled" 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") 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") 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") 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 # Internal URLs
FOG_BASE_URL = "https://static.demarches-simplifiees.fr" 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 :demandes, only: [:index]
resources :bill_signatures, only: [:index]
resources :services, only: [:index, :show] resources :services, only: [:index, :show]
post 'demandes/create_administrateur' post 'demandes/create_administrateur'

View file

@ -64,6 +64,8 @@ defaults: &defaults
crisp: crisp:
enabled: <%= ENV['CRISP_ENABLED'] == 'enabled' %> enabled: <%= ENV['CRISP_ENABLED'] == 'enabled' %>
client_key: <%= ENV['CRISP_CLIENT_KEY'] %> client_key: <%= ENV['CRISP_CLIENT_KEY'] %>
universign:
userpwd: <%= ENV['UNIVERSIGN_USERPWD'] %>
@ -90,6 +92,8 @@ test:
token_endpoint: https://bidon.com/endpoint token_endpoint: https://bidon.com/endpoint
userinfo_endpoint: https://bidon.com/endpoint userinfo_endpoint: https://bidon.com/endpoint
logout_endpoint: https://bidon.com/endpoint logout_endpoint: https://bidon.com/endpoint
universign:
userpwd: 'fake:fake'
# Do not keep production secrets in the repository, # Do not keep production secrets in the repository,
# instead read values from the environment. # 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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" 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" t.index ["gestionnaire_id"], name: "index_avis_on_gestionnaire_id"
end 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| create_table "champs", id: :serial, force: :cascade do |t|
t.string "value" t.string "value"
t.integer "type_de_champ_id" 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 "keep_until"
t.datetime "executed_at" t.datetime "executed_at"
t.text "digest" t.text "digest"
t.bigint "bill_signature_id"
t.index ["administration_id"], name: "index_dossier_operation_logs_on_administration_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 ["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"
t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until" 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 "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", "administrations"
add_foreign_key "dossier_operation_logs", "bill_signatures"
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"

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

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