diff --git a/.editorconfig b/.editorconfig index 16ff5720c..111c6ec90 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/README.md b/README.md index e8267b183..f113289d4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/controllers/manager/bill_signatures_controller.rb b/app/controllers/manager/bill_signatures_controller.rb new file mode 100644 index 000000000..8e454c083 --- /dev/null +++ b/app/controllers/manager/bill_signatures_controller.rb @@ -0,0 +1,4 @@ +module Manager + class BillSignaturesController < Manager::ApplicationController + end +end diff --git a/app/dashboards/bill_signature_dashboard.rb b/app/dashboards/bill_signature_dashboard.rb new file mode 100644 index 000000000..91e5d7d1f --- /dev/null +++ b/app/dashboards/bill_signature_dashboard.rb @@ -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 diff --git a/app/fields/attachment_field.rb b/app/fields/attachment_field.rb new file mode 100644 index 000000000..2022fb355 --- /dev/null +++ b/app/fields/attachment_field.rb @@ -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 diff --git a/app/jobs/operations_signature_job.rb b/app/jobs/operations_signature_job.rb new file mode 100644 index 000000000..092aec740 --- /dev/null +++ b/app/jobs/operations_signature_job.rb @@ -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 diff --git a/app/lib/asn1/timestamp.rb b/app/lib/asn1/timestamp.rb new file mode 100644 index 000000000..6dd65e4db --- /dev/null +++ b/app/lib/asn1/timestamp.rb @@ -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 diff --git a/app/lib/universign/api.rb b/app/lib/universign/api.rb new file mode 100644 index 000000000..eeb987ca1 --- /dev/null +++ b/app/lib/universign/api.rb @@ -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 diff --git a/app/models/bill_signature.rb b/app/models/bill_signature.rb new file mode 100644 index 000000000..cef20c232 --- /dev/null +++ b/app/models/bill_signature.rb @@ -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 diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index e517dcb36..a66492ace 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -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) diff --git a/app/services/bill_signature_service.rb b/app/services/bill_signature_service.rb new file mode 100644 index 000000000..f8640a4b1 --- /dev/null +++ b/app/services/bill_signature_service.rb @@ -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 diff --git a/app/views/fields/attachment_field/_index.html.haml b/app/views/fields/attachment_field/_index.html.haml new file mode 100644 index 000000000..d29687a35 --- /dev/null +++ b/app/views/fields/attachment_field/_index.html.haml @@ -0,0 +1 @@ += link_to(field.to_s, field.blob_path) diff --git a/app/views/manager/bill_signatures/_collection.html.erb b/app/views/manager/bill_signatures/_collection.html.erb new file mode 100644 index 000000000..aea9f28c7 --- /dev/null +++ b/app/views/manager/bill_signatures/_collection.html.erb @@ -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 +%> + + + + + <% collection_presenter.attribute_types.each do |attr_name, attr_type| %> + + <% end %> + <% [valid_action?(:edit, collection_presenter.resource_name), + valid_action?(:destroy, collection_presenter.resource_name)].count(true).times do %> + + <% end %> + + + + + <% resources.each do |resource| %> + + <% collection_presenter.attributes_for(resource).each do |attribute| %> + + <% end %> + + <% end %> + +
+ <%= 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) %> + + + + <% end %> + <% end %> +
+ <%= render_field attribute %> +
diff --git a/config/env.example b/config/env.example index b1aa41f94..84fd7aeb7 100644 --- a/config/env.example +++ b/config/env.example @@ -66,3 +66,6 @@ TRUSTED_NETWORKS="" SKYLIGHT_AUTHENTICATION_KEY="" LOGRAGE_ENABLED="disabled" + +UNIVERSIGN_API_URL="" +UNIVERSIGN_USERPWD="" diff --git a/config/initializers/urls.rb b/config/initializers/urls.rb index 92a27a505..585f11740 100644 --- a/config/initializers/urls.rb +++ b/config/initializers/urls.rb @@ -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" diff --git a/config/locales/models/bill_signature/fr.yml b/config/locales/models/bill_signature/fr.yml new file mode 100644 index 000000000..b614025fe --- /dev/null +++ b/config/locales/models/bill_signature/fr.yml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index da1c66ecc..e6006507c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' diff --git a/config/secrets.yml b/config/secrets.yml index 9a74a71cc..7f3166b96 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -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. diff --git a/db/migrate/20190616141702_create_bill_signature.rb b/db/migrate/20190616141702_create_bill_signature.rb new file mode 100644 index 000000000..95b0b9a2e --- /dev/null +++ b/db/migrate/20190616141702_create_bill_signature.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 0a29eae67..f44e8df90 100644 --- a/db/schema.rb +++ b/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" diff --git a/spec/factories/bill_signature.rb b/spec/factories/bill_signature.rb new file mode 100644 index 000000000..363aafb8f --- /dev/null +++ b/spec/factories/bill_signature.rb @@ -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 diff --git a/spec/factories/dossier_operation_log.rb b/spec/factories/dossier_operation_log.rb new file mode 100644 index 000000000..ff6a80a53 --- /dev/null +++ b/spec/factories/dossier_operation_log.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :dossier_operation_log do + operation { :passer_en_instruction } + end +end diff --git a/spec/fixtures/cassettes/universign.yml b/spec/fixtures/cassettes/universign.yml new file mode 100644 index 000000000..3f84c27d9 --- /dev/null +++ b/spec/fixtures/cassettes/universign.yml @@ -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 diff --git a/spec/fixtures/files/bill_signature/serialized.json b/spec/fixtures/files/bill_signature/serialized.json new file mode 100644 index 000000000..87852f06f --- /dev/null +++ b/spec/fixtures/files/bill_signature/serialized.json @@ -0,0 +1 @@ +{"dossier1": "hash1", "dossier2": "hash2"} diff --git a/spec/fixtures/files/bill_signature/signature.der b/spec/fixtures/files/bill_signature/signature.der new file mode 100644 index 000000000..9adcc9a7e Binary files /dev/null and b/spec/fixtures/files/bill_signature/signature.der differ diff --git a/spec/lib/asn1/timestamp_spec.rb b/spec/lib/asn1/timestamp_spec.rb new file mode 100644 index 000000000..3ae0d6272 --- /dev/null +++ b/spec/lib/asn1/timestamp_spec.rb @@ -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 diff --git a/spec/lib/universign/api_spec.rb b/spec/lib/universign/api_spec.rb new file mode 100644 index 000000000..124076ba5 --- /dev/null +++ b/spec/lib/universign/api_spec.rb @@ -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 diff --git a/spec/models/bill_signature_spec.rb b/spec/models/bill_signature_spec.rb new file mode 100644 index 000000000..30157cbfe --- /dev/null +++ b/spec/models/bill_signature_spec.rb @@ -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 diff --git a/spec/services/bill_signature_service_spec.rb b/spec/services/bill_signature_service_spec.rb new file mode 100644 index 000000000..2a88478df --- /dev/null +++ b/spec/services/bill_signature_service_spec.rb @@ -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