diff --git a/.rubocop.yml b/.rubocop.yml index 165bc73a..03fb8158 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -66,7 +66,10 @@ RSpec/ExampleLength: Max: 40 RSpec/MultipleMemoizedHelpers: - Max: 6 + Max: 7 + +RSpec/LetSetup: + Enabled: false Metrics/BlockNesting: Max: 4 diff --git a/app/controllers/api/tools_controller.rb b/app/controllers/api/tools_controller.rb index 32c59808..7b2d5cb4 100644 --- a/app/controllers/api/tools_controller.rb +++ b/app/controllers/api/tools_controller.rb @@ -20,10 +20,7 @@ module Api pdf = HexaPDF::Document.new(io: StringIO.new(file)) trusted_certs = Accounts.load_trusted_certs(current_account) - - is_checksum_found = ActiveStorage::Attachment.joins(:blob) - .where(name: 'documents', record_type: 'Submitter') - .exists?(blob: { checksum: Digest::MD5.base64digest(file) }) + is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file))) render json: { checksum_status: is_checksum_found ? 'verified' : 'not_found', diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index cfedfee4..8ca357e5 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -6,6 +6,8 @@ class ProcessSubmitterCompletionJob def perform(params = {}) submitter = Submitter.find(params['submitter_id']) + create_completed_submitter!(submitter) + is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil) if !is_all_completed && submitter.submission.submitters_order_preserved? @@ -24,9 +26,33 @@ class ProcessSubmitterCompletionJob enqueue_completed_emails(submitter) end + create_completed_documents!(submitter) + enqueue_completed_webhooks(submitter, is_all_completed:) end + def create_completed_submitter!(submitter) + submission = submitter.submission + sms_count = submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count + completed_submitter = CompletedSubmitter.where(submitter_id: submitter.id).first_or_initialize + completed_submitter.assign_attributes( + submission_id: submitter.submission_id, + account_id: submission.account_id, + template_id: submission.template_id, + source: submission.source, + sms_count:, + completed_at: submitter.completed_at + ) + + completed_submitter.save! + end + + def create_completed_documents!(submitter) + submitter.documents.map { |s| s.metadata['sha256'] }.compact_blank.each do |sha256| + CompletedDocument.where(submitter_id: submitter.id, sha256:).first_or_create! + end + end + def enqueue_completed_webhooks(submitter, is_all_completed: false) webhook_config = Accounts.load_webhook_config(submitter.account) diff --git a/app/models/completed_document.rb b/app/models/completed_document.rb new file mode 100644 index 00000000..fb4bac39 --- /dev/null +++ b/app/models/completed_document.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: completed_documents +# +# id :bigint not null, primary key +# sha256 :string not null +# created_at :datetime not null +# updated_at :datetime not null +# submitter_id :bigint not null +# +# Indexes +# +# index_completed_documents_on_sha256 (sha256) +# index_completed_documents_on_submitter_id (submitter_id) +# +class CompletedDocument < ApplicationRecord + belongs_to :submitter +end diff --git a/app/models/completed_submitter.rb b/app/models/completed_submitter.rb new file mode 100644 index 00000000..1018baf0 --- /dev/null +++ b/app/models/completed_submitter.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: completed_submitters +# +# id :bigint not null, primary key +# completed_at :datetime not null +# sms_count :integer not null +# source :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# submission_id :bigint not null +# submitter_id :bigint not null +# template_id :bigint not null +# +# Indexes +# +# index_completed_submitters_on_account_id (account_id) +# +class CompletedSubmitter < ApplicationRecord + belongs_to :submitter + belongs_to :submission + belongs_to :account + belongs_to :template +end diff --git a/app/views/shared/_powered_by.html.erb b/app/views/shared/_powered_by.html.erb index 8ec49114..dd0ee3fd 100644 --- a/app/views/shared/_powered_by.html.erb +++ b/app/views/shared/_powered_by.html.erb @@ -1,6 +1,6 @@
<% if local_assigns[:with_counter] %> - <% count = Submitter.where.not(completed_at: nil).distinct.count(:submission_id) %> + <% count = CompletedSubmitter.distinct.count(:submission_id) %> <% if count > 1 %> <%= t('count_documents_signed_with_html', count:) %> <% else %> diff --git a/db/migrate/20241018115034_create_completed_submitters_and_documents.rb b/db/migrate/20241018115034_create_completed_submitters_and_documents.rb new file mode 100644 index 00000000..e804b436 --- /dev/null +++ b/db/migrate/20241018115034_create_completed_submitters_and_documents.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class CreateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2] + def change + create_table :completed_submitters do |t| + t.bigint :submitter_id, null: false + t.bigint :submission_id, null: false + t.bigint :account_id, null: false, index: true + t.bigint :template_id, null: false + t.string :source, null: false + t.integer :sms_count, null: false + t.datetime :completed_at, null: false + + t.timestamps + end + + create_table :completed_documents do |t| + t.bigint :submitter_id, null: false, index: true + t.string :sha256, null: false, index: true + + t.timestamps + end + end +end diff --git a/db/migrate/20241022125135_populate_completed_submitters_and_documents.rb b/db/migrate/20241022125135_populate_completed_submitters_and_documents.rb new file mode 100644 index 00000000..01acd4aa --- /dev/null +++ b/db/migrate/20241022125135_populate_completed_submitters_and_documents.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2] + disable_ddl_transaction + + class MigrationSubmitter < ApplicationRecord + self.table_name = 'submitters' + + belongs_to :submission, class_name: 'MigrationSubmission' + has_many :submission_events, class_name: 'MigrationSubmissionEvent', foreign_key: :submitter_id + end + + class MigrationSubmission < ApplicationRecord + self.table_name = 'submissions' + end + + class MigrationSubmissionEvent < ApplicationRecord + self.table_name = 'submission_events' + end + + class MigrationCompletedSubmitter < ApplicationRecord + self.table_name = 'completed_submitters' + end + + class MigrationCompletedDocument < ApplicationRecord + self.table_name = 'completed_documents' + end + + def up + completed_submitters = MigrationSubmitter.where.not(completed_at: nil) + + completed_submitters.order(created_at: :asc).preload(:submission).find_each do |submitter| + submission = submitter.submission + sms_count = submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count + completed_submitter = MigrationCompletedSubmitter.where(submitter_id: submitter.id).first_or_initialize + completed_submitter.assign_attributes( + submission_id: submitter.submission_id, + account_id: submission.account_id, + template_id: submission.template_id, + source: submission.source, + sms_count:, + completed_at: submitter.completed_at, + created_at: submitter.completed_at, + updated_at: submitter.completed_at + ) + + completed_submitter.save! + end + + ActiveStorage::Attachment.where(record_id: completed_submitters.select(:id), + record_type: 'Submitter', + name: 'documents') + .order(created_at: :asc) + .find_each do |attachment| + sha256 = attachment.metadata['sha256'] + submitter_id = attachment.record_id + + next if sha256.blank? + + completed_document = MigrationCompletedDocument.where(submitter_id:, sha256:).first_or_initialize + completed_document.assign_attributes( + created_at: attachment.created_at, + updated_at: attachment.created_at + ) + + completed_document.save! + end + end + + def down + nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 8a3123bc..79b893b8 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[7.1].define(version: 2024_08_20_180922) do +ActiveRecord::Schema[7.2].define(version: 2024_10_22_125135) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -89,6 +89,28 @@ ActiveRecord::Schema[7.1].define(version: 2024_08_20_180922) do t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end + create_table "completed_documents", force: :cascade do |t| + t.bigint "submitter_id", null: false + t.string "sha256", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["sha256"], name: "index_completed_documents_on_sha256" + t.index ["submitter_id"], name: "index_completed_documents_on_submitter_id" + end + + create_table "completed_submitters", force: :cascade do |t| + t.bigint "submitter_id", null: false + t.bigint "submission_id", null: false + t.bigint "account_id", null: false + t.bigint "template_id", null: false + t.string "source", null: false + t.integer "sms_count", null: false + t.datetime "completed_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_completed_submitters_on_account_id" + end + create_table "document_generation_events", force: :cascade do |t| t.bigint "submitter_id", null: false t.string "event_name", null: false diff --git a/spec/factories/completed_documents.rb b/spec/factories/completed_documents.rb new file mode 100644 index 00000000..d670e9a3 --- /dev/null +++ b/spec/factories/completed_documents.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :completed_document do + submitter + sha256 { SecureRandom.hex(32) } + end +end diff --git a/spec/jobs/process_submitter_completion_job_spec.rb b/spec/jobs/process_submitter_completion_job_spec.rb new file mode 100644 index 00000000..7d084d57 --- /dev/null +++ b/spec/jobs/process_submitter_completion_job_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ProcessSubmitterCompletionJob, sidekiq: :inline, type: :job do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:submission) { create(:submission, template:, created_by_user: user) } + let(:submitter) { create(:submitter, submission:, uuid: SecureRandom.uuid, completed_at: Time.current) } + let!(:encrypted_config) do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + it 'creates a completed submitter' do + expect do + described_class.perform_async('submitter_id' => submitter.id) + end.to change(CompletedSubmitter, :count).by(1) + + completed_submitter = CompletedSubmitter.last + submitter.reload + + expect(completed_submitter.submitter_id).to eq(submitter.id) + expect(completed_submitter.submission_id).to eq(submitter.submission_id) + expect(completed_submitter.account_id).to eq(submitter.submission.account_id) + expect(completed_submitter.template_id).to eq(submitter.submission.template_id) + expect(completed_submitter.source).to eq(submitter.submission.source) + end + + it 'creates a completed document' do + expect do + described_class.perform_async('submitter_id' => submitter.id) + end.to change(CompletedDocument, :count).by(1) + + completed_document = CompletedDocument.last + + expect(completed_document.submitter_id).to eq(submitter.id) + expect(completed_document.sha256).to be_present + expect(completed_document.sha256).to eq(submitter.documents.first.metadata['sha256']) + end + + it 'raises an error if the submitter is not found' do + expect do + described_class.perform_async('submitter_id' => 'invalid_id') + end.to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/lib/params/base_validator_spec.rb b/spec/lib/params/base_validator_spec.rb index b729941a..c7152bda 100644 --- a/spec/lib/params/base_validator_spec.rb +++ b/spec/lib/params/base_validator_spec.rb @@ -36,6 +36,7 @@ RSpec.describe Params::BaseValidator do 'jone.doe@', 'this...is@strange.but.valid.com', 'user@-weird-domain-.com', + 'user.name@[IPv6:2001:db8::1]', 'tricky.email@sub.example-.com' ] diff --git a/spec/requests/tools_spec.rb b/spec/requests/tools_spec.rb new file mode 100644 index 00000000..6ddd66be --- /dev/null +++ b/spec/requests/tools_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Tools API', type: :request do + let!(:account) { create(:account) } + let!(:author) { create(:user, account:) } + let!(:file_path) { Rails.root.join('spec/fixtures/sample-document.pdf') } + let!(:encrypted_config) do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe 'POST /api/tools/verify' do + it 'returns a verification result' do + template = create(:template, account:, author:) + submission = create(:submission, :with_submitters, :with_events, template:, created_by_user: author) + blob = ActiveStorage::Blob.create_and_upload!( + io: file_path.open, + filename: 'sample-document.pdf', + content_type: 'application/pdf' + ) + create(:completed_document, submitter: submission.submitters.first, + sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(blob.download))) + + ActiveStorage::Attachment.create!( + blob:, + name: :documents, + record: submission.submitters.first + ) + + post '/api/tools/verify', headers: { 'x-auth-token': author.access_token.token }, params: { + file: Base64.encode64(File.read(file_path)) + }.to_json + + expect(response).to have_http_status(:ok) + expect(response.parsed_body['checksum_status']).to eq('verified') + end + end +end diff --git a/spec/system/submit_form_spec.rb b/spec/system/submit_form_spec.rb index da4c5463..e3e979ec 100644 --- a/spec/system/submit_form_spec.rb +++ b/spec/system/submit_form_spec.rb @@ -6,6 +6,10 @@ RSpec.describe 'Submit Form' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } let!(:template) { create(:template, account:, author: user) } + let!(:encrypted_config) do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end before do sign_in(user) @@ -72,16 +76,5 @@ RSpec.describe 'Submit Form' do expect(submitter.completed_at).to be_present expect(submitter.values.values).to include('Sally') end - - it 'sends completed email' do - fill_in 'First Name', with: 'Adam' - click_on 'next' - click_on 'type_text_button' - fill_in 'signature_text_input', with: 'Adam' - - expect do - click_on 'Sign and Complete' - end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1) - end end end