mirror of https://github.com/docusealco/docuseal
Merge pull request #10 from CareerPlug/CP-10359
CP-10359 - Add ATS prefill field extraction for template and submission on viewspull/544/head
commit
2c1258023d
@ -0,0 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PrefillFieldsHelper
|
||||
# Cache TTL for ATS field parsing (1 hour)
|
||||
ATS_FIELDS_CACHE_TTL = 1.hour
|
||||
|
||||
# Maximum number of cached entries to prevent memory bloat
|
||||
MAX_CACHE_ENTRIES = 1000
|
||||
|
||||
def extract_ats_prefill_fields
|
||||
return [] if params[:ats_fields].blank?
|
||||
|
||||
# Create cache key from parameter hash for security and uniqueness
|
||||
cache_key = ats_fields_cache_key(params[:ats_fields])
|
||||
|
||||
# Try to get from cache first with error handling
|
||||
begin
|
||||
cached_result = Rails.cache.read(cache_key)
|
||||
if cached_result
|
||||
Rails.logger.debug { "ATS fields cache hit for key: #{cache_key}" }
|
||||
return cached_result
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn "Cache read failed for ATS fields: #{e.message}"
|
||||
# Continue with normal processing if cache read fails
|
||||
end
|
||||
|
||||
# Cache miss - perform expensive operations
|
||||
Rails.logger.debug { "ATS fields cache miss for key: #{cache_key}" }
|
||||
|
||||
begin
|
||||
decoded_json = Base64.urlsafe_decode64(params[:ats_fields])
|
||||
field_names = JSON.parse(decoded_json)
|
||||
|
||||
# Validate that we got an array of strings
|
||||
return cache_and_return_empty(cache_key) unless field_names.is_a?(Array) && field_names.all?(String)
|
||||
|
||||
# Filter to only expected field name patterns
|
||||
valid_fields = field_names.select { |name| valid_ats_field_name?(name) }
|
||||
|
||||
# Cache the result with TTL (with error handling)
|
||||
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
|
||||
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_result(cache_key, [], 5.minutes)
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Clear ATS fields cache (useful for testing or manual cache invalidation)
|
||||
def clear_ats_fields_cache
|
||||
# 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
|
||||
Rails.logger.info 'ATS fields cache clear requested (relies on TTL for cleanup)'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_ats_field_name?(name)
|
||||
# Only allow expected field name patterns (security)
|
||||
name.match?(/\A(employee|manager|account|location)_[a-z_]+\z/)
|
||||
end
|
||||
|
||||
def ats_fields_cache_key(ats_fields_param)
|
||||
# Create secure cache key using SHA256 hash of the parameter
|
||||
# This prevents cache key collisions and keeps keys reasonably sized
|
||||
hash = Digest::SHA256.hexdigest(ats_fields_param)
|
||||
"ats_fields:#{hash}"
|
||||
end
|
||||
|
||||
def cache_result(cache_key, value, ttl)
|
||||
Rails.cache.write(cache_key, value, expires_in: ttl)
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn "Cache write failed for ATS fields: #{e.message}"
|
||||
# Continue execution even if caching fails
|
||||
end
|
||||
|
||||
def cache_and_return_empty(cache_key)
|
||||
cache_result(cache_key, [], 5.minutes)
|
||||
[]
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,350 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PrefillFieldsHelper, type: :helper do
|
||||
# Clear cache before each test to ensure clean state
|
||||
before do
|
||||
Rails.cache.clear
|
||||
end
|
||||
|
||||
describe '#extract_ats_prefill_fields' do
|
||||
it 'extracts valid field names from base64 encoded parameter' do
|
||||
fields = %w[employee_first_name employee_email manager_firstname]
|
||||
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
|
||||
|
||||
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
|
||||
expect(result).to eq(%w[employee_first_name account_name])
|
||||
end
|
||||
|
||||
it 'returns empty array when no ats_fields parameter' do
|
||||
allow(helper).to receive(:params).and_return({})
|
||||
|
||||
result = helper.extract_ats_prefill_fields
|
||||
expect(result).to eq([])
|
||||
end
|
||||
|
||||
it 'returns empty array when ats_fields parameter is empty' do
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: '' })
|
||||
|
||||
result = helper.extract_ats_prefill_fields
|
||||
expect(result).to eq([])
|
||||
end
|
||||
|
||||
it 'returns empty array when decoded JSON is not an array' do
|
||||
not_array = Base64.urlsafe_encode64({ field: 'employee_name' }.to_json)
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: not_array })
|
||||
|
||||
result = helper.extract_ats_prefill_fields
|
||||
expect(result).to eq([])
|
||||
end
|
||||
|
||||
it 'returns empty array when array contains non-string values' do
|
||||
mixed_array = ['employee_first_name', 123, 'manager_firstname']
|
||||
encoded = Base64.urlsafe_encode64(mixed_array.to_json)
|
||||
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
|
||||
|
||||
result = helper.extract_ats_prefill_fields
|
||||
expect(result).to eq([])
|
||||
end
|
||||
|
||||
it 'accepts all valid field name patterns' do
|
||||
fields = %w[
|
||||
employee_first_name
|
||||
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)
|
||||
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
|
||||
|
||||
result = helper.extract_ats_prefill_fields
|
||||
expect(result).to eq(fields)
|
||||
end
|
||||
|
||||
it 'logs successful field reception on cache miss' do
|
||||
fields = %w[employee_first_name employee_email]
|
||||
encoded = Base64.urlsafe_encode64(fields.to_json)
|
||||
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
|
||||
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
|
||||
|
||||
it 'logs parsing errors and caches empty result' do
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: 'invalid_base64' })
|
||||
allow(Rails.logger).to receive(:warn)
|
||||
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
|
||||
describe 'caching behavior' do
|
||||
let(:fields) { %w[employee_first_name employee_email manager_firstname] }
|
||||
let(:encoded) { Base64.urlsafe_encode64(fields.to_json) }
|
||||
|
||||
# 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
|
||||
|
||||
it 'caches successful parsing results' do
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
|
||||
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
|
||||
cache_key = helper.send(:ats_fields_cache_key, encoded)
|
||||
cached_value = Rails.cache.read(cache_key)
|
||||
expect(cached_value).to eq(fields)
|
||||
end
|
||||
|
||||
it 'returns cached results on subsequent calls' do
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
|
||||
allow(Rails.logger).to receive(:info)
|
||||
allow(Rails.logger).to receive(:debug)
|
||||
|
||||
# First call - cache miss
|
||||
result1 = helper.extract_ats_prefill_fields
|
||||
expect(result1).to eq(fields)
|
||||
|
||||
# Verify cache miss was logged
|
||||
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
|
||||
|
||||
it 'caches empty results for parsing errors' do
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: 'invalid_base64' })
|
||||
allow(Rails.logger).to receive(:warn)
|
||||
allow(Rails.logger).to receive(:debug)
|
||||
|
||||
# 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
|
||||
|
||||
it 'generates consistent cache keys for same input' do
|
||||
key1 = helper.send(:ats_fields_cache_key, encoded)
|
||||
key2 = helper.send(:ats_fields_cache_key, encoded)
|
||||
|
||||
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
|
||||
|
||||
it 'generates different cache keys for different inputs' do
|
||||
fields2 = %w[manager_lastname location_name]
|
||||
encoded2 = Base64.urlsafe_encode64(fields2.to_json)
|
||||
|
||||
key1 = helper.send(:ats_fields_cache_key, encoded)
|
||||
key2 = helper.send(:ats_fields_cache_key, encoded2)
|
||||
|
||||
expect(key1).not_to eq(key2)
|
||||
end
|
||||
|
||||
it 'respects cache TTL for successful results' do
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
|
||||
allow(Rails.cache).to receive(:write).and_call_original
|
||||
|
||||
helper.extract_ats_prefill_fields
|
||||
|
||||
expect(Rails.cache).to have_received(:write).with(
|
||||
anything,
|
||||
fields,
|
||||
expires_in: PrefillFieldsHelper::ATS_FIELDS_CACHE_TTL
|
||||
)
|
||||
end
|
||||
|
||||
it 'uses shorter TTL for error results' do
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: 'invalid_base64' })
|
||||
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(
|
||||
anything,
|
||||
[],
|
||||
expires_in: 5.minutes
|
||||
)
|
||||
end
|
||||
|
||||
it 'handles cache read failures gracefully' do
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
|
||||
allow(Rails.cache).to receive(:read).and_raise(StandardError.new('Cache error'))
|
||||
allow(Rails.logger).to receive(:info)
|
||||
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
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
|
||||
allow(Rails.cache).to receive(:write).and_raise(StandardError.new('Cache error'))
|
||||
allow(Rails.logger).to receive(:info)
|
||||
allow(Rails.logger).to receive(:debug)
|
||||
allow(Rails.logger).to receive(:warn)
|
||||
|
||||
# Should still return correct result even if caching fails
|
||||
result = helper.extract_ats_prefill_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
|
||||
|
||||
it 'avoids expensive operations on cache hits' do
|
||||
allow(helper).to receive(:params).and_return({ ats_fields: encoded })
|
||||
allow(Rails.logger).to receive(:info)
|
||||
allow(Rails.logger).to receive(:debug)
|
||||
|
||||
# First call to populate cache
|
||||
helper.extract_ats_prefill_fields
|
||||
|
||||
# Mock expensive operations to verify they're not called on cache hit
|
||||
allow(Base64).to receive(:urlsafe_decode64).and_call_original
|
||||
allow(JSON).to receive(:parse).and_call_original
|
||||
|
||||
# Second call should use cache
|
||||
result = helper.extract_ats_prefill_fields
|
||||
expect(result).to eq(fields)
|
||||
|
||||
# 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
|
||||
|
||||
describe '#valid_ats_field_name?' do
|
||||
it 'returns true for valid employee field names' do
|
||||
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
|
||||
expect(helper.send(:valid_ats_field_name?, 'manager_firstname')).to be true
|
||||
expect(helper.send(:valid_ats_field_name?, 'manager_lastname')).to be true
|
||||
expect(helper.send(:valid_ats_field_name?, 'manager_email')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for valid account field names' do
|
||||
expect(helper.send(:valid_ats_field_name?, 'account_name')).to be true
|
||||
expect(helper.send(:valid_ats_field_name?, 'account_id')).to be true
|
||||
end
|
||||
|
||||
it 'returns true for valid location field names' do
|
||||
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
|
||||
expect(helper.send(:valid_ats_field_name?, 'malicious_field')).to be false
|
||||
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
|
||||
Loading…
Reference in new issue