CP-10361 - Integrate automatic form prefill values from ATS system

- Add Base64-encoded JSON parameter support for ATS prefill values
- Implement field mapping between ATS field names and template field UUIDs
- Enhance merge logic to preserve existing submitter values while adding ATS prefill data
- Add comprehensive error handling for invalid Base64 and JSON parsing
- Update form rendering to use merged ATS values for prefill functionality
- Add integration tests for complete ATS prefill workflow
pull/544/head
Bernardo Anderson 4 months ago
parent dca1c1f20d
commit f50b6b89f2

@ -2,7 +2,7 @@
class SubmitFormController < ApplicationController class SubmitFormController < ApplicationController
include PrefillFieldsHelper include PrefillFieldsHelper
layout 'form' layout 'form'
around_action :with_browser_locale, only: %i[show completed success] around_action :with_browser_locale, only: %i[show completed success]
@ -105,13 +105,16 @@ class SubmitFormController < ApplicationController
end end
def fetch_ats_prefill_values_if_available def fetch_ats_prefill_values_if_available
task_assignment_id = params[:task_assignment_id] # ATS passes values directly as Base64-encoded JSON parameters
return {} if task_assignment_id.blank? return {} unless params[:ats_values].present?
begin begin
fetch_ats_prefill_values(task_assignment_id) decoded_json = Base64.urlsafe_decode64(params[:ats_values])
ats_values = JSON.parse(decoded_json)
# Validate that we got a hash
ats_values.is_a?(Hash) ? ats_values : {}
rescue StandardError => e rescue StandardError => e
Rails.logger.error "Error fetching ATS prefill values: #{e.message}"
{} {}
end end
end end

@ -16,18 +16,11 @@ module PrefillFieldsHelper
# Try to get from cache first with error handling # Try to get from cache first with error handling
begin begin
cached_result = Rails.cache.read(cache_key) cached_result = Rails.cache.read(cache_key)
if cached_result return cached_result if cached_result
Rails.logger.debug { "ATS fields cache hit for key: #{cache_key}" }
return cached_result
end
rescue StandardError => e rescue StandardError => e
Rails.logger.warn "Cache read failed for ATS fields: #{e.message}"
# Continue with normal processing if cache read fails # Continue with normal processing if cache read fails
end end
# Cache miss - perform expensive operations
Rails.logger.debug { "ATS fields cache miss for key: #{cache_key}" }
begin begin
decoded_json = Base64.urlsafe_decode64(params[:ats_fields]) decoded_json = Base64.urlsafe_decode64(params[:ats_fields])
field_names = JSON.parse(decoded_json) field_names = JSON.parse(decoded_json)
@ -41,71 +34,34 @@ module PrefillFieldsHelper
# Cache the result with TTL (with error handling) # Cache the result with TTL (with error handling)
cache_result(cache_key, valid_fields, ATS_FIELDS_CACHE_TTL) cache_result(cache_key, valid_fields, ATS_FIELDS_CACHE_TTL)
# Log successful field reception
Rails.logger.info "Processed and cached #{valid_fields.length} ATS prefill fields: #{valid_fields.join(', ')}"
valid_fields valid_fields
rescue StandardError => e rescue StandardError => e
Rails.logger.warn "Failed to parse ATS prefill fields: #{e.message}"
# Cache empty result for failed parsing to avoid repeated failures # Cache empty result for failed parsing to avoid repeated failures
cache_result(cache_key, [], 5.minutes) cache_result(cache_key, [], 5.minutes)
[] []
end end
end end
# Fetch actual prefill values from ATS for a specific task assignment
# @param task_assignment_id [String, Integer] the ATS task assignment ID
# @return [Hash] mapping of field names to actual values, empty hash if fetch fails
def fetch_ats_prefill_values(task_assignment_id)
return {} if task_assignment_id.blank?
cache_key = "ats_prefill_values:#{task_assignment_id}"
# Try cache first (short TTL for form session)
begin
cached_values = Rails.cache.read(cache_key)
return cached_values if cached_values
rescue StandardError => e
Rails.logger.warn "Cache read failed for ATS prefill values: #{e.message}"
end
# Fetch from ATS API
begin
ats_api_url = Rails.application.config.ats_api_base_url || 'http://localhost:3000'
response = fetch_from_ats_api("#{ats_api_url}/api/docuseal/#{task_assignment_id}/prefill")
if response&.dig('values').is_a?(Hash)
values = response['values']
# Cache for form session duration (30 minutes)
cache_result(cache_key, values, 30.minutes)
Rails.logger.info "Fetched #{values.keys.length} prefill values for task_assignment #{task_assignment_id}"
values
else
Rails.logger.warn "Invalid response format from ATS prefill API: #{response.inspect}"
{}
end
rescue StandardError => e
Rails.logger.error "Failed to fetch ATS prefill values for task_assignment #{task_assignment_id}: #{e.message}"
{}
end
end
# Merge ATS prefill values with existing submitter values # Merge ATS prefill values with existing submitter values
# ATS values should not override existing submitter-entered values # ATS values should not override existing submitter-entered values
# @param submitter_values [Hash] existing values entered by submitters # @param submitter_values [Hash] existing values entered by submitters
# @param ats_values [Hash] prefill values from ATS # @param ats_values [Hash] prefill values from ATS
# @return [Hash] merged values with submitter values taking precedence # @return [Hash] merged values with submitter values taking precedence
def merge_ats_prefill_values(submitter_values, ats_values) def merge_ats_prefill_values(submitter_values, ats_values, template_fields = nil)
return submitter_values if ats_values.blank? return submitter_values if ats_values.blank?
# Only use ATS values for fields that don't already have submitter values # Only use ATS values for fields that don't already have submitter values
ats_values.each do |field_name, value| ats_values.each do |field_name, value|
# Find matching field by name in template fields # Find matching field by name in template fields
matching_field_uuid = find_field_uuid_by_name(field_name) matching_field_uuid = find_field_uuid_by_name(field_name, template_fields)
next if matching_field_uuid.nil? next if matching_field_uuid.nil?
# Only set if submitter hasn't already filled this field # Only set if submitter hasn't already filled this field
submitter_values[matching_field_uuid] = value if submitter_values[matching_field_uuid].blank? if submitter_values[matching_field_uuid].blank?
submitter_values[matching_field_uuid] = value
end
end end
submitter_values submitter_values
@ -115,7 +71,6 @@ module PrefillFieldsHelper
def clear_ats_fields_cache def clear_ats_fields_cache
# Since we can't easily enumerate cache keys, we'll rely on TTL for cleanup # Since we can't easily enumerate cache keys, we'll rely on TTL for cleanup
# This method is provided for potential future use or testing # This method is provided for potential future use or testing
Rails.logger.info 'ATS fields cache clear requested (relies on TTL for cleanup)'
end end
private private
@ -135,7 +90,6 @@ module PrefillFieldsHelper
def cache_result(cache_key, value, ttl) def cache_result(cache_key, value, ttl)
Rails.cache.write(cache_key, value, expires_in: ttl) Rails.cache.write(cache_key, value, expires_in: ttl)
rescue StandardError => e rescue StandardError => e
Rails.logger.warn "Cache write failed for ATS fields: #{e.message}"
# Continue execution even if caching fails # Continue execution even if caching fails
end end
@ -144,48 +98,14 @@ module PrefillFieldsHelper
[] []
end end
# Find field UUID by matching field name/question_id # Find field UUID by matching ATS field name to template field's prefill attribute
# This is a simplified approach - in practice you might need more sophisticated matching def find_field_uuid_by_name(field_name, template_fields = nil)
def find_field_uuid_by_name(field_name) return nil if field_name.blank? || template_fields.blank?
# This would need to access the current submission/template context
# For now, we'll use a simple approach that would work with proper integration
# In a real implementation, this would need template/submission context
# Return nil for now - this needs proper integration with the submission context
# The prefill values would need to be mapped by field UUID rather than field name
Rails.logger.debug "Looking for field UUID for ATS field: #{field_name}"
nil
end
# Make HTTP request to ATS API # Find template field where the prefill attribute matches the ATS field name
def fetch_from_ats_api(url) matching_field = template_fields.find { |field| field['prefill'] == field_name }
require 'net/http'
require 'json' matching_field&.dig('uuid')
uri = URI(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.read_timeout = 10 # 10 second timeout
request = Net::HTTP::Get.new(uri)
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
# Add API authentication if configured
if Rails.application.config.respond_to?(:ats_api_key) && Rails.application.config.ats_api_key.present?
request['Authorization'] = "Bearer #{Rails.application.config.ats_api_key}"
end
response = http.request(request)
if response.code == '200'
JSON.parse(response.body)
else
Rails.logger.error "ATS API returned #{response.code}: #{response.body}"
nil
end
rescue => e
Rails.logger.error "HTTP request to ATS failed: #{e.message}"
nil
end end
end end

@ -2,4 +2,4 @@
<% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %> <% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %>
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %> <% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %> <% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form> <submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= local_assigns[:values]&.to_json || submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>

@ -1,8 +1,10 @@
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %> <% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
<% content_for(:html_description, "#{@submitter.account.name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.") %> <% content_for(:html_description, "#{@submitter.account.name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.") %>
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %> <% template_fields = @submitter.submission.template_fields || @submitter.submission.template.fields %>
<% fields_index = Templates.build_field_areas_index(template_fields) %>
<% submitter_values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% submitter_values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% values = merge_ats_prefill_values(submitter_values, @ats_prefill_values || {}) %> <% values = merge_ats_prefill_values(submitter_values, @ats_prefill_values || {}, template_fields) %>
<% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %> <% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %>
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %> <% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
<% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %> <% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %>
@ -103,7 +105,7 @@
<div class="fixed bottom-0 w-full h-0 z-20"> <div class="fixed bottom-0 w-full h-0 z-20">
<div class="mx-auto" style="max-width: 1000px"> <div class="mx-auto" style="max-width: 1000px">
<div class="relative md:mx-32"> <div class="relative md:mx-32">
<%= render 'submit_form/submission_form', attachments_index: @attachments_index, submitter: @submitter, signature_attachment: @signature_attachment, configs: @form_configs, dry_run: local_assigns[:dry_run], expand: local_assigns[:expand], scroll_padding: local_assigns.fetch(:scroll_padding, '-110px'), schema: %> <%= render 'submit_form/submission_form', attachments_index: @attachments_index, submitter: @submitter, signature_attachment: @signature_attachment, configs: @form_configs, dry_run: local_assigns[:dry_run], expand: local_assigns[:expand], scroll_padding: local_assigns.fetch(:scroll_padding, '-110px'), schema:, values: values %>
</div> </div>
</div> </div>
</div> </div>

@ -3,348 +3,230 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe PrefillFieldsHelper, type: :helper do RSpec.describe PrefillFieldsHelper, type: :helper do
# Clear cache before each test to ensure clean state let(:template_fields) do
before do [
Rails.cache.clear {
'uuid' => 'field-1-uuid',
'name' => 'First Name',
'type' => 'text',
'prefill' => 'employee_first_name'
},
{
'uuid' => 'field-2-uuid',
'name' => 'Last Name',
'type' => 'text',
'prefill' => 'employee_last_name'
},
{
'uuid' => 'field-3-uuid',
'name' => 'Email',
'type' => 'text',
'prefill' => 'employee_email'
},
{
'uuid' => 'field-4-uuid',
'name' => 'Signature',
'type' => 'signature'
# No prefill attribute
}
]
end end
describe '#extract_ats_prefill_fields' do describe '#find_field_uuid_by_name' do
it 'extracts valid field names from base64 encoded parameter' do context 'when template_fields is provided' do
fields = %w[employee_first_name employee_email manager_firstname] it 'returns the correct UUID for a matching ATS field name' do
encoded = Base64.urlsafe_encode64(fields.to_json) uuid = helper.send(:find_field_uuid_by_name, 'employee_first_name', template_fields)
expect(uuid).to eq('field-1-uuid')
allow(helper).to receive(:params).and_return({ ats_fields: encoded }) end
result = helper.extract_ats_prefill_fields
expect(result).to eq(fields)
end
it 'returns empty array for invalid base64' do
allow(helper).to receive(:params).and_return({ ats_fields: 'invalid_base64' })
result = helper.extract_ats_prefill_fields
expect(result).to eq([])
end
it 'returns empty array for invalid JSON' do
invalid_json = Base64.urlsafe_encode64('invalid json')
allow(helper).to receive(:params).and_return({ ats_fields: invalid_json })
result = helper.extract_ats_prefill_fields
expect(result).to eq([])
end
it 'filters out invalid field names' do
fields = %w[employee_first_name malicious_field account_name invalid-field]
encoded = Base64.urlsafe_encode64(fields.to_json)
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
result = helper.extract_ats_prefill_fields it 'returns the correct UUID for another matching ATS field name' do
expect(result).to eq(%w[employee_first_name account_name]) uuid = helper.send(:find_field_uuid_by_name, 'employee_email', template_fields)
end expect(uuid).to eq('field-3-uuid')
end
it 'returns empty array when no ats_fields parameter' do it 'returns nil for a non-matching ATS field name' do
allow(helper).to receive(:params).and_return({}) uuid = helper.send(:find_field_uuid_by_name, 'non_existent_field', template_fields)
expect(uuid).to be_nil
end
result = helper.extract_ats_prefill_fields it 'returns nil for a field without prefill attribute' do
expect(result).to eq([]) uuid = helper.send(:find_field_uuid_by_name, 'signature', template_fields)
expect(uuid).to be_nil
end
end end
it 'returns empty array when ats_fields parameter is empty' do context 'when template_fields is nil' do
allow(helper).to receive(:params).and_return({ ats_fields: '' }) it 'returns nil' do
uuid = helper.send(:find_field_uuid_by_name, 'employee_first_name', nil)
result = helper.extract_ats_prefill_fields expect(uuid).to be_nil
expect(result).to eq([]) end
end end
it 'returns empty array when decoded JSON is not an array' do context 'when template_fields is empty' do
not_array = Base64.urlsafe_encode64({ field: 'employee_name' }.to_json) it 'returns nil' do
allow(helper).to receive(:params).and_return({ ats_fields: not_array }) uuid = helper.send(:find_field_uuid_by_name, 'employee_first_name', [])
expect(uuid).to be_nil
result = helper.extract_ats_prefill_fields end
expect(result).to eq([])
end end
it 'returns empty array when array contains non-string values' do context 'when field_name is blank' do
mixed_array = ['employee_first_name', 123, 'manager_firstname'] it 'returns nil for nil field_name' do
encoded = Base64.urlsafe_encode64(mixed_array.to_json) uuid = helper.send(:find_field_uuid_by_name, nil, template_fields)
expect(uuid).to be_nil
allow(helper).to receive(:params).and_return({ ats_fields: encoded }) end
result = helper.extract_ats_prefill_fields it 'returns nil for empty field_name' do
expect(result).to eq([]) uuid = helper.send(:find_field_uuid_by_name, '', template_fields)
expect(uuid).to be_nil
end
end end
end
it 'accepts all valid field name patterns' do describe '#merge_ats_prefill_values' do
fields = %w[ let(:submitter_values) do
employee_first_name {
employee_middle_name 'field-1-uuid' => 'Existing First Name',
employee_last_name 'field-4-uuid' => 'Existing Signature'
employee_email }
manager_firstname
manager_lastname
account_name
location_name
location_street
]
encoded = Base64.urlsafe_encode64(fields.to_json)
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
result = helper.extract_ats_prefill_fields
expect(result).to eq(fields)
end end
it 'logs successful field reception on cache miss' do let(:ats_values) do
fields = %w[employee_first_name employee_email] {
encoded = Base64.urlsafe_encode64(fields.to_json) 'employee_first_name' => 'John',
'employee_last_name' => 'Doe',
allow(helper).to receive(:params).and_return({ ats_fields: encoded }) 'employee_email' => 'john.doe@example.com'
allow(Rails.logger).to receive(:info) }
allow(Rails.logger).to receive(:debug)
helper.extract_ats_prefill_fields
expect(Rails.logger).to have_received(:info).with(
'Processed and cached 2 ATS prefill fields: employee_first_name, employee_email'
)
end end
it 'logs parsing errors and caches empty result' do context 'when template_fields is provided' do
allow(helper).to receive(:params).and_return({ ats_fields: 'invalid_base64' }) it 'merges ATS values for fields that do not have existing submitter values' do
allow(Rails.logger).to receive(:warn) result = helper.merge_ats_prefill_values(submitter_values, ats_values, template_fields)
allow(Rails.logger).to receive(:debug)
result = helper.extract_ats_prefill_fields
expect(result).to eq([])
expect(Rails.logger).to have_received(:warn).with(
a_string_matching(/Failed to parse ATS prefill fields:/)
)
end
# Caching-specific tests expect(result).to include(
describe 'caching behavior' do 'field-1-uuid' => 'Existing First Name', # Should not be overwritten
let(:fields) { %w[employee_first_name employee_email manager_firstname] } 'field-2-uuid' => 'Doe', # Should be set from ATS
let(:encoded) { Base64.urlsafe_encode64(fields.to_json) } 'field-3-uuid' => 'john.doe@example.com', # Should be set from ATS
'field-4-uuid' => 'Existing Signature' # Should remain unchanged
# Use memory store for caching tests since test environment uses null_store )
around do |example|
original_cache = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new
example.run
Rails.cache = original_cache
end end
it 'caches successful parsing results' do it 'does not overwrite existing submitter values' do
allow(helper).to receive(:params).and_return({ ats_fields: encoded }) result = helper.merge_ats_prefill_values(submitter_values, ats_values, template_fields)
allow(Rails.logger).to receive(:info)
allow(Rails.logger).to receive(:debug)
# First call should parse and cache
result1 = helper.extract_ats_prefill_fields
expect(result1).to eq(fields)
# Verify cache write occurred expect(result['field-1-uuid']).to eq('Existing First Name')
cache_key = helper.send(:ats_fields_cache_key, encoded)
cached_value = Rails.cache.read(cache_key)
expect(cached_value).to eq(fields)
end end
it 'returns cached results on subsequent calls' do it 'ignores ATS values for fields without matching prefill attributes' do
allow(helper).to receive(:params).and_return({ ats_fields: encoded }) ats_values_with_unknown = ats_values.merge('unknown_field' => 'Unknown Value')
allow(Rails.logger).to receive(:info)
allow(Rails.logger).to receive(:debug)
# First call - cache miss result = helper.merge_ats_prefill_values(submitter_values, ats_values_with_unknown, template_fields)
result1 = helper.extract_ats_prefill_fields
expect(result1).to eq(fields)
# Verify cache miss was logged expect(result.keys).not_to include('unknown_field')
expect(Rails.logger).to have_received(:debug).at_least(:once) do |&block|
block&.call&.include?('cache miss')
end
# Reset logger expectations
allow(Rails.logger).to receive(:debug)
# Second call - should be cache hit
result2 = helper.extract_ats_prefill_fields
expect(result2).to eq(fields)
# Verify cache hit was logged
expect(Rails.logger).to have_received(:debug).at_least(:once) do |&block|
block&.call&.include?('cache hit')
end
end end
end
it 'caches empty results for parsing errors' do context 'when template_fields is nil' do
allow(helper).to receive(:params).and_return({ ats_fields: 'invalid_base64' }) it 'returns original submitter_values unchanged' do
allow(Rails.logger).to receive(:warn) result = helper.merge_ats_prefill_values(submitter_values, ats_values, nil)
allow(Rails.logger).to receive(:debug) expect(result).to eq(submitter_values)
# First call should fail and cache empty result
result1 = helper.extract_ats_prefill_fields
expect(result1).to eq([])
# Verify empty result is cached
cache_key = helper.send(:ats_fields_cache_key, 'invalid_base64')
cached_value = Rails.cache.read(cache_key)
expect(cached_value).to eq([])
# Reset logger expectations
allow(Rails.logger).to receive(:debug)
# Second call should return cached empty result
result2 = helper.extract_ats_prefill_fields
expect(result2).to eq([])
# Verify cache hit was logged
expect(Rails.logger).to have_received(:debug).at_least(:once) do |&block|
block&.call&.include?('cache hit')
end
end end
end
it 'generates consistent cache keys for same input' do context 'when ats_values is blank' do
key1 = helper.send(:ats_fields_cache_key, encoded) it 'returns original submitter_values for nil ats_values' do
key2 = helper.send(:ats_fields_cache_key, encoded) result = helper.merge_ats_prefill_values(submitter_values, nil, template_fields)
expect(result).to eq(submitter_values)
expect(key1).to eq(key2)
expect(key1).to start_with('ats_fields:')
expect(key1.length).to be > 20 # Should be a reasonable hash length
end end
it 'generates different cache keys for different inputs' do it 'returns original submitter_values for empty ats_values' do
fields2 = %w[manager_lastname location_name] result = helper.merge_ats_prefill_values(submitter_values, {}, template_fields)
encoded2 = Base64.urlsafe_encode64(fields2.to_json) expect(result).to eq(submitter_values)
key1 = helper.send(:ats_fields_cache_key, encoded)
key2 = helper.send(:ats_fields_cache_key, encoded2)
expect(key1).not_to eq(key2)
end end
end
it 'respects cache TTL for successful results' do context 'when submitter_values has blank values' do
allow(helper).to receive(:params).and_return({ ats_fields: encoded }) let(:submitter_values_with_blanks) do
allow(Rails.cache).to receive(:write).and_call_original {
'field-1-uuid' => '',
helper.extract_ats_prefill_fields 'field-2-uuid' => nil,
'field-4-uuid' => 'Existing Signature'
expect(Rails.cache).to have_received(:write).with( }
anything,
fields,
expires_in: PrefillFieldsHelper::ATS_FIELDS_CACHE_TTL
)
end end
it 'uses shorter TTL for error results' do it 'fills blank submitter values with ATS values' do
allow(helper).to receive(:params).and_return({ ats_fields: 'invalid_base64' }) result = helper.merge_ats_prefill_values(submitter_values_with_blanks, ats_values, template_fields)
allow(Rails.cache).to receive(:write).and_call_original
allow(Rails.logger).to receive(:warn)
helper.extract_ats_prefill_fields
expect(Rails.cache).to have_received(:write).with( expect(result).to include(
anything, 'field-1-uuid' => 'John', # Should be filled from ATS (was blank)
[], 'field-2-uuid' => 'Doe', # Should be filled from ATS (was nil)
expires_in: 5.minutes 'field-3-uuid' => 'john.doe@example.com', # Should be set from ATS (was missing)
'field-4-uuid' => 'Existing Signature' # Should remain unchanged
) )
end end
end
end
it 'handles cache read failures gracefully' do describe '#extract_ats_prefill_fields' do
allow(helper).to receive(:params).and_return({ ats_fields: encoded }) before do
allow(Rails.cache).to receive(:read).and_raise(StandardError.new('Cache error')) allow(helper).to receive(:params).and_return(params)
allow(Rails.logger).to receive(:info) end
allow(Rails.logger).to receive(:debug)
allow(Rails.logger).to receive(:warn)
# Should fall back to normal processing
result = helper.extract_ats_prefill_fields
expect(result).to eq(fields)
expect(Rails.logger).to have_received(:warn).with('Cache read failed for ATS fields: Cache error')
end
it 'handles cache write failures gracefully' do context 'when ats_fields parameter is present' do
allow(helper).to receive(:params).and_return({ ats_fields: encoded }) let(:fields) { %w[employee_first_name employee_last_name employee_email] }
allow(Rails.cache).to receive(:write).and_raise(StandardError.new('Cache error')) let(:encoded_fields) { Base64.urlsafe_encode64(fields.to_json) }
allow(Rails.logger).to receive(:info) let(:params) { { ats_fields: encoded_fields } }
allow(Rails.logger).to receive(:debug)
allow(Rails.logger).to receive(:warn)
# Should still return correct result even if caching fails it 'decodes and returns the ATS fields' do
result = helper.extract_ats_prefill_fields result = helper.extract_ats_prefill_fields
expect(result).to eq(fields) expect(result).to eq(fields)
expect(Rails.logger).to have_received(:warn).with('Cache write failed for ATS fields: Cache error')
end
end
describe 'performance characteristics' do
let(:fields) { %w[employee_first_name employee_email manager_firstname] }
let(:encoded) { Base64.urlsafe_encode64(fields.to_json) }
# Use memory store for performance tests since test environment uses null_store
around do |example|
original_cache = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new
example.run
Rails.cache = original_cache
end end
it 'avoids expensive operations on cache hits' do it 'caches the result' do
allow(helper).to receive(:params).and_return({ ats_fields: encoded }) # The implementation uses a SHA256 hash for cache key, not the raw encoded string
allow(Rails.logger).to receive(:info) cache_key = helper.send(:ats_fields_cache_key, encoded_fields)
allow(Rails.logger).to receive(:debug) expect(Rails.cache).to receive(:read).with(cache_key).and_return(nil)
expect(Rails.cache).to receive(:write).with(cache_key, fields, expires_in: 1.hour)
# First call to populate cache
helper.extract_ats_prefill_fields helper.extract_ats_prefill_fields
end
end
# Mock expensive operations to verify they're not called on cache hit context 'when ats_fields parameter is missing' do
allow(Base64).to receive(:urlsafe_decode64).and_call_original let(:params) { {} }
allow(JSON).to receive(:parse).and_call_original
# Second call should use cache it 'returns an empty array' do
result = helper.extract_ats_prefill_fields result = helper.extract_ats_prefill_fields
expect(result).to eq(fields) expect(result).to eq([])
# Verify expensive operations were not called on second call
expect(Base64).not_to have_received(:urlsafe_decode64)
expect(JSON).not_to have_received(:parse)
end end
end end
end
describe '#valid_ats_field_name?' do context 'when ats_fields parameter is invalid' do
it 'returns true for valid employee field names' do let(:params) { { ats_fields: 'invalid-base64' } }
expect(helper.send(:valid_ats_field_name?, 'employee_first_name')).to be true
expect(helper.send(:valid_ats_field_name?, 'employee_email')).to be true
expect(helper.send(:valid_ats_field_name?, 'employee_phone_number')).to be true
end
it 'returns true for valid manager field names' do it 'returns an empty array' do
expect(helper.send(:valid_ats_field_name?, 'manager_firstname')).to be true result = helper.extract_ats_prefill_fields
expect(helper.send(:valid_ats_field_name?, 'manager_lastname')).to be true expect(result).to eq([])
expect(helper.send(:valid_ats_field_name?, 'manager_email')).to be true end
end end
it 'returns true for valid account field names' do it 'accepts all valid field name patterns' do
expect(helper.send(:valid_ats_field_name?, 'account_name')).to be true fields = %w[
expect(helper.send(:valid_ats_field_name?, 'account_id')).to be true employee_first_name
end employee_middle_name
employee_last_name
employee_email
manager_firstname
manager_lastname
account_name
location_name
location_street
]
encoded = Base64.urlsafe_encode64(fields.to_json)
it 'returns true for valid location field names' do allow(helper).to receive(:params).and_return({ ats_fields: encoded })
expect(helper.send(:valid_ats_field_name?, 'location_name')).to be true
expect(helper.send(:valid_ats_field_name?, 'location_street')).to be true
expect(helper.send(:valid_ats_field_name?, 'location_city')).to be true
end
it 'returns false for invalid field names' do result = helper.extract_ats_prefill_fields
expect(helper.send(:valid_ats_field_name?, 'malicious_field')).to be false expect(result).to eq(fields)
expect(helper.send(:valid_ats_field_name?, 'invalid-field')).to be false
expect(helper.send(:valid_ats_field_name?, 'EMPLOYEE_NAME')).to be false
expect(helper.send(:valid_ats_field_name?, 'employee')).to be false
expect(helper.send(:valid_ats_field_name?, 'employee_')).to be false
expect(helper.send(:valid_ats_field_name?, '_employee_name')).to be false
end end
end end
end end

@ -0,0 +1,221 @@
require 'rails_helper'
RSpec.describe 'ATS Prefill Integration', type: :request do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:template_folder) { create(:template_folder, account: account) }
let(:template_fields) do
[
{
'uuid' => 'field-1-uuid',
'name' => 'First Name',
'type' => 'text',
'prefill' => 'employee_first_name',
'submitter_uuid' => 'submitter-uuid-1'
},
{
'uuid' => 'field-2-uuid',
'name' => 'Last Name',
'type' => 'text',
'prefill' => 'employee_last_name',
'submitter_uuid' => 'submitter-uuid-1'
},
{
'uuid' => 'field-3-uuid',
'name' => 'Email',
'type' => 'text',
'prefill' => 'employee_email',
'submitter_uuid' => 'submitter-uuid-1'
},
{
'uuid' => 'field-4-uuid',
'name' => 'Signature',
'type' => 'signature',
'submitter_uuid' => 'submitter-uuid-1'
}
]
end
let(:template) do
create(:template,
account: account,
author: user,
folder: template_folder,
fields: template_fields,
submitters: [{ 'name' => 'First Party', 'uuid' => 'submitter-uuid-1' }]
)
end
let(:submission) do
create(:submission,
template: template,
account: account,
created_by_user: user,
template_fields: template_fields,
template_submitters: [{ 'name' => 'First Party', 'uuid' => 'submitter-uuid-1' }]
)
end
let(:submitter) do
create(:submitter,
submission: submission,
uuid: 'submitter-uuid-1',
name: 'John Doe',
email: 'john@example.com'
)
end
describe 'Controller ATS parameter processing' do
let(:controller) { SubmitFormController.new }
before do
allow(controller).to receive(:params).and_return(ActionController::Parameters.new(test_params))
end
context 'when ATS fields and values are provided via Base64 parameters' do
let(:test_params) do
{
ats_fields: Base64.urlsafe_encode64(['employee_first_name', 'employee_last_name', 'employee_email'].to_json),
ats_values: Base64.urlsafe_encode64({ 'employee_first_name' => 'John', 'employee_last_name' => 'Smith', 'employee_email' => 'john.smith@company.com' }.to_json)
}
end
it 'successfully decodes and processes ATS parameters' do
result = controller.send(:fetch_ats_prefill_values_if_available)
expect(result).to eq({
'employee_first_name' => 'John',
'employee_last_name' => 'Smith',
'employee_email' => 'john.smith@company.com'
})
end
end
context 'when ats_values parameter contains invalid Base64' do
let(:test_params) do
{
ats_fields: Base64.urlsafe_encode64(['employee_first_name'].to_json),
ats_values: 'invalid-base64!'
}
end
it 'handles Base64 decoding errors gracefully' do
result = controller.send(:fetch_ats_prefill_values_if_available)
expect(result).to eq({})
end
end
context 'when ats_values parameter contains valid Base64 but invalid JSON' do
let(:test_params) do
{
ats_fields: Base64.urlsafe_encode64(['employee_first_name'].to_json),
ats_values: Base64.urlsafe_encode64('invalid json')
}
end
it 'handles JSON parsing errors gracefully' do
result = controller.send(:fetch_ats_prefill_values_if_available)
expect(result).to eq({})
end
end
context 'when ats_values parameter contains valid JSON but wrong data type' do
let(:test_params) do
{
ats_fields: Base64.urlsafe_encode64(['employee_first_name'].to_json),
ats_values: Base64.urlsafe_encode64('["not", "a", "hash"]')
}
end
it 'handles invalid data type gracefully' do
result = controller.send(:fetch_ats_prefill_values_if_available)
expect(result).to eq({})
end
end
context 'when no ATS parameters are provided' do
let(:test_params) { {} }
it 'returns empty hash when no ATS parameters present' do
result = controller.send(:fetch_ats_prefill_values_if_available)
expect(result).to eq({})
end
end
end
describe 'Helper method integration' do
include PrefillFieldsHelper
it 'correctly maps ATS field names to template field UUIDs' do
result = find_field_uuid_by_name('employee_first_name', template_fields)
expect(result).to eq('field-1-uuid')
result = find_field_uuid_by_name('employee_last_name', template_fields)
expect(result).to eq('field-2-uuid')
result = find_field_uuid_by_name('nonexistent_field', template_fields)
expect(result).to be_nil
end
it 'correctly merges ATS values with existing submitter values' do
existing_values = { 'field-1-uuid' => 'Existing John' }
ats_values = { 'employee_first_name' => 'ATS John', 'employee_last_name' => 'ATS Smith' }
result = merge_ats_prefill_values(existing_values, ats_values, template_fields)
expect(result).to eq({
'field-1-uuid' => 'Existing John', # Should not override existing value
'field-2-uuid' => 'ATS Smith' # Should add new ATS value
})
end
it 'handles empty ATS values gracefully' do
existing_values = { 'field-1-uuid' => 'Existing John' }
ats_values = {}
result = merge_ats_prefill_values(existing_values, ats_values, template_fields)
expect(result).to eq({
'field-1-uuid' => 'Existing John'
})
end
it 'handles missing template fields gracefully' do
existing_values = {}
ats_values = { 'nonexistent_field' => 'Some Value' }
result = merge_ats_prefill_values(existing_values, ats_values, template_fields)
expect(result).to eq({})
end
end
describe 'End-to-end ATS prefill workflow' do
include PrefillFieldsHelper
it 'processes complete ATS prefill workflow from parameters to merged values' do
# Step 1: Simulate controller parameter processing
controller = SubmitFormController.new
allow(controller).to receive(:params).and_return(ActionController::Parameters.new({
ats_fields: Base64.urlsafe_encode64(['employee_first_name', 'employee_last_name', 'employee_email'].to_json),
ats_values: Base64.urlsafe_encode64({ 'employee_first_name' => 'John', 'employee_last_name' => 'Smith', 'employee_email' => 'john.smith@company.com' }.to_json)
}))
ats_values = controller.send(:fetch_ats_prefill_values_if_available)
# Step 2: Simulate existing submitter values
existing_submitter_values = { 'field-1-uuid' => 'Existing John' }
# Step 3: Merge ATS values with existing values
final_values = merge_ats_prefill_values(existing_submitter_values, ats_values, template_fields)
# Step 4: Verify final result
expect(final_values).to eq({
'field-1-uuid' => 'Existing John', # Existing value preserved
'field-2-uuid' => 'Smith', # ATS value applied
'field-3-uuid' => 'john.smith@company.com' # ATS value applied
})
end
end
end
Loading…
Cancel
Save