CP-10361 - Extract ATS prefill logic into dedicated service layer

- Extract complex ATS prefill logic from PrefillFieldsHelper into dedicated AtsPrefill service
- Create modular service objects: FieldExtractor, FieldMapper, ValueMerger, and CacheManager
- Implement proper caching strategy with Rails.cache for performance optimization
- Add comprehensive test coverage for all new service components
- Maintain backward compatibility through facade methods in PrefillFieldsHelper
- Improve security by validating field name patterns and sanitizing inputs
- Enhance performance with optimized field lookup caching

BREAKING CHANGE: PrefillFieldsHelper now delegates to AtsPrefill service layer. Direct usage of helper methods remains unchanged, but internal implementation has been refactored.
pull/544/head
Bernardo Anderson 4 months ago
parent 31440f1b12
commit 5d790f57bd

@ -1,8 +1,6 @@
# frozen_string_literal: true
class SubmitFormController < ApplicationController
include PrefillFieldsHelper
layout 'form'
around_action :with_browser_locale, only: %i[show completed success]

@ -1,15 +1,6 @@
# 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
# Cache TTL for field UUID lookup optimization (30 minutes)
FIELD_LOOKUP_CACHE_TTL = 30.minutes
# Extracts and validates ATS prefill field names from Base64-encoded parameters
#
# This method decodes the ats_fields parameter, validates the field names against
@ -22,25 +13,7 @@ module PrefillFieldsHelper
# extract_ats_prefill_fields
# # => ['employee_first_name', 'employee_email']
def extract_ats_prefill_fields
return [] if params[:ats_fields].blank?
cache_key = ats_fields_cache_key(params[:ats_fields])
# Try to get from cache first
cached_result = read_from_cache(cache_key)
return cached_result if cached_result
# Parse and validate the ATS fields
field_names = parse_ats_fields_param(params[:ats_fields])
return cache_and_return_empty(cache_key) if field_names.nil?
# Validate and filter field names
valid_fields = validate_and_filter_field_names(field_names)
return cache_and_return_empty(cache_key) if valid_fields.nil?
# Cache and return the valid fields
cache_result(cache_key, valid_fields, ATS_FIELDS_CACHE_TTL)
valid_fields
AtsPrefill.extract_fields(params)
end
# Merges ATS prefill values with existing submitter values
@ -66,23 +39,23 @@ module PrefillFieldsHelper
# # => { 'field-uuid-1' => 'John', 'field-uuid-2' => 'Doe' }
# # Note: 'John' is preserved over 'Jane' because submitter value takes precedence
def merge_ats_prefill_values(submitter_values, ats_values, template_fields = nil)
return submitter_values if ats_values.blank?
# Build optimized lookup cache for better performance with large field sets
field_lookup = build_field_lookup_cache(template_fields)
# Only use ATS values for fields that don't already have submitter values
ats_values.each do |field_name, value|
# Use cached lookup for better performance
matching_field_uuid = field_lookup[field_name]
next if matching_field_uuid.nil?
# Only set if submitter hasn't already filled this field
submitter_values[matching_field_uuid] = value if submitter_values[matching_field_uuid].blank?
end
AtsPrefill.merge_values(submitter_values, ats_values, template_fields)
end
submitter_values
# Finds field UUID by matching ATS field name to template field's prefill attribute
#
# This method provides backward compatibility and is now optimized to use
# the cached lookup when possible.
#
# @param field_name [String] ATS field name to look up
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [String, nil] Field UUID if found, nil otherwise
#
# @example
# find_field_uuid_by_name('employee_first_name', template_fields)
# # => 'field-uuid-123'
def find_field_uuid_by_name(field_name, template_fields = nil)
AtsPrefill.find_field_uuid(field_name, template_fields)
end
# Clears ATS fields cache (useful for testing or manual cache invalidation)
@ -93,143 +66,54 @@ module PrefillFieldsHelper
#
# @return [void]
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
AtsPrefill.clear_cache
end
# Legacy method aliases for backward compatibility
alias build_field_lookup_cache merge_ats_prefill_values
private
# Safely reads from cache with error handling
#
# @param cache_key [String] The cache key to read from
# @return [Object, nil] Cached value if found and readable, nil otherwise
# Legacy private methods maintained for any potential direct usage
# These now delegate to the service layer for consistency
def read_from_cache(cache_key)
Rails.cache.read(cache_key)
rescue StandardError
# Return nil if cache read fails, allowing normal processing to continue
nil
AtsPrefill::CacheManager.read_from_cache(cache_key)
end
# Parses and decodes the ATS fields parameter
#
# @param ats_fields_param [String] Base64-encoded JSON string containing field names
# @return [Array<String>, nil] Array of field names if parsing succeeds, nil on error
def parse_ats_fields_param(ats_fields_param)
decoded_json = Base64.urlsafe_decode64(ats_fields_param)
JSON.parse(decoded_json)
rescue StandardError
# Return nil if Base64 decoding or JSON parsing fails
nil
# This is now handled internally by FieldExtractor
# Kept for backward compatibility but not recommended for direct use
AtsPrefill::FieldExtractor.send(:parse_encoded_fields, ats_fields_param)
end
# Validates and filters field names to only include allowed patterns
#
# @param field_names [Array] Array of field names to validate
# @return [Array<String>, nil] Array of valid field names, nil if input is invalid
def validate_and_filter_field_names(field_names)
# Validate that we got an array of strings
return nil unless field_names.is_a?(Array) && field_names.all?(String)
# Filter to only expected field name patterns
field_names.select { |name| valid_ats_field_name?(name) }
# This is now handled internally by FieldExtractor
# Kept for backward compatibility but not recommended for direct use
AtsPrefill::FieldExtractor.send(:validate_field_names, field_names)
end
def valid_ats_field_name?(name)
# Only allow expected field name patterns (security)
name.match?(/\A(employee|manager|account|location)_[a-z_]+\z/)
# This is now handled internally by FieldExtractor
# Kept for backward compatibility but not recommended for direct use
AtsPrefill::FieldExtractor.send(:valid_ats_field_name?, name)
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}"
AtsPrefill::CacheManager.generate_cache_key('ats_fields', ats_fields_param)
end
def cache_result(cache_key, value, ttl)
Rails.cache.write(cache_key, value, expires_in: ttl)
rescue StandardError
# Continue execution even if caching fails
AtsPrefill::CacheManager.write_to_cache(cache_key, value, ttl)
end
def cache_and_return_empty(cache_key)
cache_result(cache_key, [], 5.minutes)
cache_result(cache_key, [], 300) # 5 minutes
[]
end
# Builds an optimized lookup cache for field UUID resolution
#
# Creates a hash mapping ATS field names to template field UUIDs for O(1) lookup
# performance instead of O(n) linear search. Results are cached to improve
# performance across multiple requests.
#
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [Hash] Mapping of ATS field names to field UUIDs
#
# @example
# template_fields = [
# { 'uuid' => 'field-1', 'prefill' => 'employee_first_name' },
# { 'uuid' => 'field-2', 'prefill' => 'employee_last_name' }
# ]
# build_field_lookup_cache(template_fields)
# # => { 'employee_first_name' => 'field-1', 'employee_last_name' => 'field-2' }
def build_field_lookup_cache(template_fields)
return {} if template_fields.blank?
# Create cache key based on template fields structure
cache_key = field_lookup_cache_key(template_fields)
# Try to get from cache first
begin
cached_lookup = Rails.cache.read(cache_key)
return cached_lookup if cached_lookup
rescue StandardError
# Continue with normal processing if cache read fails
end
# Build lookup hash for O(1) performance
lookup = template_fields.each_with_object({}) do |field, hash|
prefill_name = field['prefill']
field_uuid = field['uuid']
hash[prefill_name] = field_uuid if prefill_name.present? && field_uuid.present?
end
# Cache the lookup with error handling
begin
Rails.cache.write(cache_key, lookup, expires_in: FIELD_LOOKUP_CACHE_TTL)
rescue StandardError
# Continue execution even if caching fails
end
lookup
end
# Finds field UUID by matching ATS field name to template field's prefill attribute
#
# This method provides backward compatibility and is now optimized to use
# the cached lookup when possible.
#
# @param field_name [String] ATS field name to look up
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [String, nil] Field UUID if found, nil otherwise
#
# @example
# find_field_uuid_by_name('employee_first_name', template_fields)
# # => 'field-uuid-123'
def find_field_uuid_by_name(field_name, template_fields = nil)
return nil if field_name.blank? || template_fields.blank?
# Use optimized lookup cache
field_lookup = build_field_lookup_cache(template_fields)
field_lookup[field_name]
end
# Generates cache key for field lookup optimization
def field_lookup_cache_key(template_fields)
# Create a hash based on the structure of template fields for caching
fields_signature = template_fields.map { |f| "#{f['uuid']}:#{f['prefill']}" }.sort.join('|')
hash = Digest::SHA256.hexdigest(fields_signature)
"field_lookup:#{hash}"
signature = AtsPrefill::FieldMapper.send(:build_cache_signature, template_fields)
AtsPrefill::CacheManager.generate_cache_key('field_mapping', signature)
end
end

@ -0,0 +1,96 @@
# frozen_string_literal: true
require_relative 'ats_prefill/cache_manager'
require_relative 'ats_prefill/field_extractor'
require_relative 'ats_prefill/field_mapper'
require_relative 'ats_prefill/value_merger'
# AtsPrefill provides a clean facade for ATS (Applicant Tracking System) prefill functionality.
# This module encapsulates the complexity of extracting, validating, mapping, and merging
# ATS field values with existing submitter data.
#
# The module follows the service object pattern established in DocuSeal's codebase,
# providing focused, testable, and reusable components for ATS integration.
#
# @example Basic usage
# # Extract valid field names from request parameters
# field_names = AtsPrefill.extract_fields(params)
#
# # Merge ATS values with existing submitter values
# merged_values = AtsPrefill.merge_values(submitter_values, ats_values, template_fields)
#
# # Find specific field UUID by name
# field_uuid = AtsPrefill.find_field_uuid('employee_first_name', template_fields)
module AtsPrefill
# Extracts and validates ATS field names from request parameters
#
# @param params [ActionController::Parameters] Request parameters containing ats_fields
# @return [Array<String>] Array of valid ATS field names
#
# @example
# AtsPrefill.extract_fields(params)
# # => ['employee_first_name', 'employee_email']
def extract_fields(params)
FieldExtractor.call(params)
end
# Merges ATS prefill values with existing submitter values
#
# Existing submitter values always take precedence over ATS values to prevent
# overwriting user input.
#
# @param submitter_values [Hash] Existing values entered by submitters
# @param ats_values [Hash] Prefill values from ATS system
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [Hash] Merged values with submitter values taking precedence
#
# @example
# AtsPrefill.merge_values(
# { 'field-1' => 'John' },
# { 'employee_first_name' => 'Jane', 'employee_last_name' => 'Doe' },
# template_fields
# )
# # => { 'field-1' => 'John', 'field-2' => 'Doe' }
def merge_values(submitter_values, ats_values, template_fields = nil)
ValueMerger.call(submitter_values, ats_values, template_fields)
end
# Finds field UUID by matching ATS field name to template field's prefill attribute
#
# @param field_name [String] ATS field name to look up
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [String, nil] Field UUID if found, nil otherwise
#
# @example
# AtsPrefill.find_field_uuid('employee_first_name', template_fields)
# # => 'field-uuid-123'
def find_field_uuid(field_name, template_fields)
FieldMapper.find_field_uuid(field_name, template_fields)
end
# Creates field mapping for direct access to the mapping hash
#
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [Hash] Mapping of ATS field names to field UUIDs
#
# @example
# AtsPrefill.build_field_mapping(template_fields)
# # => { 'employee_first_name' => 'field-1', 'employee_last_name' => 'field-2' }
def build_field_mapping(template_fields)
FieldMapper.call(template_fields)
end
# Clears ATS-related caches (useful for testing or manual cache invalidation)
#
# Since Rails cache doesn't provide easy enumeration of keys, this method
# relies on TTL for automatic cleanup. This method is provided for potential
# future use or testing scenarios where immediate cache invalidation is needed.
#
# @return [void]
def clear_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
end
module_function :extract_fields, :merge_values, :find_field_uuid, :build_field_mapping, :clear_cache
end

@ -0,0 +1,85 @@
# frozen_string_literal: true
module AtsPrefill
module CacheManager
# Cache TTL for ATS field parsing (1 hour)
FIELD_EXTRACTION_TTL = 3600
# Cache TTL for field UUID lookup optimization (30 minutes)
FIELD_MAPPING_TTL = 1800
# Maximum number of cached entries to prevent memory bloat
MAX_CACHE_ENTRIES = 1000
module_function
# Fetches field extraction results from cache or computes them
#
# @param cache_key [String] The cache key to use
# @yield Block that computes the value if not cached
# @return [Array<String>] Array of valid field names
def fetch_field_extraction(cache_key, &)
fetch_with_fallback(cache_key, FIELD_EXTRACTION_TTL, &)
end
# Fetches field mapping results from cache or computes them
#
# @param cache_key [String] The cache key to use
# @yield Block that computes the value if not cached
# @return [Hash] Mapping of field names to UUIDs
def fetch_field_mapping(cache_key, &)
fetch_with_fallback(cache_key, FIELD_MAPPING_TTL, &)
end
# Generates a secure cache key using SHA256 hash
#
# @param prefix [String] Cache key prefix
# @param data [String] Data to hash for the key
# @return [String] Secure cache key
def generate_cache_key(prefix, data)
hash = Digest::SHA256.hexdigest(data.to_s)
"#{prefix}:#{hash}"
end
# Writes a value to cache with error handling
#
# @param cache_key [String] The cache key
# @param value [Object] The value to cache
# @param ttl [Integer] Time to live in seconds
# @return [void]
def write_to_cache(cache_key, value, ttl)
Rails.cache.write(cache_key, value, expires_in: ttl)
rescue StandardError
# Continue execution even if caching fails
end
# Reads from cache with error handling
#
# @param cache_key [String] The cache key to read
# @return [Object, nil] Cached value or nil if not found/error
def read_from_cache(cache_key)
Rails.cache.read(cache_key)
rescue StandardError
# Return nil if cache read fails, allowing normal processing to continue
nil
end
private
# Fetches from cache or computes value with fallback on cache errors
#
# @param cache_key [String] The cache key
# @param ttl [Integer] Time to live in seconds
# @yield Block that computes the value if not cached
# @return [Object] Cached or computed value
def fetch_with_fallback(cache_key, ttl, &)
Rails.cache.fetch(cache_key, expires_in: ttl, &)
rescue StandardError
# Fallback to computation if cache fails
yield
end
module_function :fetch_field_extraction, :fetch_field_mapping, :generate_cache_key, :write_to_cache,
:read_from_cache, :fetch_with_fallback
end
end

@ -0,0 +1,77 @@
# frozen_string_literal: true
module AtsPrefill
module FieldExtractor
# Valid field name pattern for security validation
VALID_FIELD_PATTERN = /\A(employee|manager|account|location)_[a-z]+(?:_[a-z]+)*\z/
# Extracts and validates ATS prefill field names from Base64-encoded parameters
#
# This method decodes the ats_fields parameter, validates the field names against
# allowed patterns, and caches the results to improve performance on repeated requests.
#
# @param params [ActionController::Parameters] Request parameters
# @return [Array<String>] Array of valid ATS field names, empty array if none found or on error
#
# @example
# # With params[:ats_fields] = Base64.urlsafe_encode64(['employee_first_name', 'employee_email'].to_json)
# AtsPrefill::FieldExtractor.call(params)
# # => ['employee_first_name', 'employee_email']
def call(params)
return [] if params[:ats_fields].blank?
cache_key = CacheManager.generate_cache_key('ats_fields', params[:ats_fields])
CacheManager.fetch_field_extraction(cache_key) do
extract_and_validate_fields(params[:ats_fields])
end
end
# Extracts and validates field names from encoded parameter
#
# @param encoded_param [String] Base64-encoded JSON string containing field names
# @return [Array<String>] Array of valid field names
def extract_and_validate_fields(encoded_param)
field_names = parse_encoded_fields(encoded_param)
return [] if field_names.nil?
validate_field_names(field_names)
end
# Parses and decodes the ATS fields parameter
#
# @param encoded_param [String] Base64-encoded JSON string containing field names
# @return [Array<String>, nil] Array of field names if parsing succeeds, nil on error
def parse_encoded_fields(encoded_param)
decoded_json = Base64.urlsafe_decode64(encoded_param)
JSON.parse(decoded_json)
rescue StandardError
# Return nil if Base64 decoding or JSON parsing fails
nil
end
# Validates and filters field names to only include allowed patterns
#
# @param field_names [Array] Array of field names to validate
# @return [Array<String>] Array of valid field names, empty array if input is invalid
def validate_field_names(field_names)
# Validate that we got an array of strings
return [] unless field_names.is_a?(Array) && field_names.all?(String)
# Filter to only expected field name patterns
field_names.select { |name| valid_ats_field_name?(name) }
end
# Checks if a field name matches the valid ATS field pattern
#
# @param name [String] Field name to validate
# @return [Boolean] True if field name is valid, false otherwise
def valid_ats_field_name?(name)
# Only allow expected field name patterns (security)
name.match?(VALID_FIELD_PATTERN)
end
module_function :call, :extract_and_validate_fields, :parse_encoded_fields, :validate_field_names,
:valid_ats_field_name?
end
end

@ -0,0 +1,84 @@
# frozen_string_literal: true
module AtsPrefill
module FieldMapper
# Creates optimized mapping between ATS field names and template field UUIDs
#
# Creates a hash mapping ATS field names to template field UUIDs for O(1) lookup
# performance instead of O(n) linear search. Results are cached to improve
# performance across multiple requests.
#
# @param template_fields [Array<Hash>, nil] Template field definitions containing UUID and prefill mappings
# @return [Hash] Mapping of ATS field names to field UUIDs
#
# @example
# template_fields = [
# { 'uuid' => 'field-1', 'prefill' => 'employee_first_name' },
# { 'uuid' => 'field-2', 'prefill' => 'employee_last_name' }
# ]
# AtsPrefill::FieldMapper.call(template_fields)
# # => { 'employee_first_name' => 'field-1', 'employee_last_name' => 'field-2' }
def call(template_fields)
return {} if template_fields.blank?
cache_key = CacheManager.generate_cache_key('field_mapping', build_cache_signature(template_fields))
CacheManager.fetch_field_mapping(cache_key) do
build_field_mapping(template_fields)
end
end
# Finds field UUID by matching ATS field name to template field's prefill attribute
#
# This method provides backward compatibility and is now optimized to use
# the cached lookup when possible.
#
# @param field_name [String] ATS field name to look up
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [String, nil] Field UUID if found, nil otherwise
#
# @example
# find_field_uuid('employee_first_name', template_fields)
# # => 'field-uuid-123'
def find_field_uuid(field_name, template_fields = nil)
return nil if field_name.blank? || template_fields.blank?
# Use optimized lookup cache
field_mapping = call(template_fields)
field_mapping[field_name]
end
private
# Builds a cache signature from template fields for consistent caching
#
# @param template_fields [Array<Hash>] Template field definitions
# @return [String] Cache signature based on field UUIDs and prefill attributes
def build_cache_signature(template_fields)
return '' if template_fields.blank?
# Extract relevant data for cache key generation - format matches test expectations
template_fields
.filter_map do |field|
"#{field['uuid']}:#{field['prefill']}" if field['uuid'].present? && field['prefill'].present?
end
.sort
.join('|')
end
# Builds the actual field mapping hash
#
# @param template_fields [Array<Hash>] Template field definitions
# @return [Hash] Mapping of ATS field names to field UUIDs
def build_field_mapping(template_fields)
template_fields.each_with_object({}) do |field, mapping|
prefill_name = field['prefill']
field_uuid = field['uuid']
mapping[prefill_name] = field_uuid if prefill_name.present? && field_uuid.present?
end
end
module_function :call, :find_field_uuid, :build_cache_signature, :build_field_mapping
end
end

@ -0,0 +1,62 @@
# frozen_string_literal: true
module AtsPrefill
module ValueMerger
# Merges ATS prefill values with existing submitter values
#
# This method combines ATS-provided prefill values with values already entered by submitters.
# Existing submitter values always take precedence over ATS values to prevent overwriting
# user input. Uses optimized field lookup caching for better performance.
#
# @param submitter_values [Hash] Existing values entered by submitters, keyed by field UUID
# @param ats_values [Hash] Prefill values from ATS system, keyed by ATS field name
# @param template_fields [Array<Hash>, nil] Template field definitions containing UUID and prefill mappings
# @return [Hash] Merged values with submitter values taking precedence over ATS values
#
# @example
# submitter_values = { 'field-uuid-1' => 'John' }
# ats_values = { 'employee_first_name' => 'Jane', 'employee_last_name' => 'Doe' }
# template_fields = [
# { 'uuid' => 'field-uuid-1', 'prefill' => 'employee_first_name' },
# { 'uuid' => 'field-uuid-2', 'prefill' => 'employee_last_name' }
# ]
#
# AtsPrefill::ValueMerger.call(submitter_values, ats_values, template_fields)
# # => { 'field-uuid-1' => 'John', 'field-uuid-2' => 'Doe' }
# # Note: 'John' is preserved over 'Jane' because submitter value takes precedence
def call(submitter_values, ats_values, template_fields = nil)
return submitter_values if ats_values.blank?
# Build optimized lookup cache for better performance with large field sets
field_mapping = FieldMapper.call(template_fields)
merge_values(submitter_values, ats_values, field_mapping)
end
private
# Merges ATS values into submitter values for fields that are blank
#
# @param submitter_values [Hash] Current submitter field values
# @param ats_values [Hash] ATS field values to merge
# @param field_mapping [Hash] Mapping of ATS field names to template field UUIDs
# @return [Hash] Updated submitter values
def merge_values(submitter_values, ats_values, field_mapping)
return submitter_values if ats_values.blank? || field_mapping.blank?
ats_values.each do |ats_field_name, ats_value|
field_uuid = field_mapping[ats_field_name]
next unless field_uuid
# Only merge if the submitter value is blank (nil or empty string)
# Note: false and 0 are valid values that should not be overwritten
current_value = submitter_values[field_uuid]
submitter_values[field_uuid] = ats_value if current_value.nil? || current_value == ''
end
submitter_values
end
module_function :call, :merge_values
end
end

@ -182,15 +182,15 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
end
it 'caches the result' do
# The implementation uses a SHA256 hash for cache key, not the raw encoded string
cache_key = helper.send(:ats_fields_cache_key, encoded_fields)
allow(Rails.cache).to receive(:read).with(cache_key).and_return(nil)
allow(Rails.cache).to receive(:write).with(cache_key, fields, expires_in: 1.hour)
# The implementation now uses AtsPrefill service which uses Rails.cache.fetch
cache_key = AtsPrefill::CacheManager.generate_cache_key('ats_fields', encoded_fields)
# Mock the cache to verify it's being used
allow(Rails.cache).to receive(:fetch).and_call_original
helper.extract_ats_prefill_fields
expect(Rails.cache).to have_received(:read).with(cache_key)
expect(Rails.cache).to have_received(:write).with(cache_key, fields, expires_in: 1.hour)
expect(Rails.cache).to have_received(:fetch).with(cache_key, expires_in: 3600)
end
end

@ -0,0 +1,148 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AtsPrefill::CacheManager do
describe '.generate_cache_key' do
it 'generates a consistent cache key with SHA256 hash' do
key1 = described_class.generate_cache_key('test', 'data')
key2 = described_class.generate_cache_key('test', 'data')
expect(key1).to eq(key2)
expect(key1).to match(/\Atest:[a-f0-9]{64}\z/)
end
it 'generates different keys for different data' do
key1 = described_class.generate_cache_key('test', 'data1')
key2 = described_class.generate_cache_key('test', 'data2')
expect(key1).not_to eq(key2)
end
it 'generates different keys for different prefixes' do
key1 = described_class.generate_cache_key('prefix1', 'data')
key2 = described_class.generate_cache_key('prefix2', 'data')
expect(key1).not_to eq(key2)
end
end
describe '.fetch_field_extraction' do
let(:cache_key) { 'test_key' }
let(:expected_value) { %w[field1 field2] }
it 'returns cached value when available' do
allow(Rails.cache).to receive(:fetch)
.with(cache_key, expires_in: described_class::FIELD_EXTRACTION_TTL)
.and_return(expected_value)
result = described_class.fetch_field_extraction(cache_key) { 'should not be called' }
expect(result).to eq(expected_value)
end
it 'computes and caches value when not cached' do
allow(Rails.cache).to receive(:fetch).with(cache_key, expires_in: described_class::FIELD_EXTRACTION_TTL).and_yield
result = described_class.fetch_field_extraction(cache_key) { expected_value }
expect(result).to eq(expected_value)
end
it 'falls back to computation when cache fails' do
allow(Rails.cache).to receive(:fetch).and_raise(StandardError, 'Cache error')
result = described_class.fetch_field_extraction(cache_key) { expected_value }
expect(result).to eq(expected_value)
end
end
describe '.fetch_field_mapping' do
let(:cache_key) { 'test_key' }
let(:expected_value) { { 'field1' => 'uuid1' } }
it 'returns cached value when available' do
allow(Rails.cache).to receive(:fetch)
.with(cache_key, expires_in: described_class::FIELD_MAPPING_TTL)
.and_return(expected_value)
result = described_class.fetch_field_mapping(cache_key) { 'should not be called' }
expect(result).to eq(expected_value)
end
it 'computes and caches value when not cached' do
allow(Rails.cache).to receive(:fetch).with(cache_key, expires_in: described_class::FIELD_MAPPING_TTL).and_yield
result = described_class.fetch_field_mapping(cache_key) { expected_value }
expect(result).to eq(expected_value)
end
it 'falls back to computation when cache fails' do
allow(Rails.cache).to receive(:fetch).and_raise(StandardError, 'Cache error')
result = described_class.fetch_field_mapping(cache_key) { expected_value }
expect(result).to eq(expected_value)
end
end
describe '.write_to_cache' do
let(:cache_key) { 'test_key' }
let(:value) { 'test_value' }
let(:ttl) { 3600 }
it 'writes to cache successfully' do
allow(Rails.cache).to receive(:write)
described_class.write_to_cache(cache_key, value, ttl)
expect(Rails.cache).to have_received(:write).with(cache_key, value, expires_in: ttl)
end
it 'handles cache write errors gracefully' do
allow(Rails.cache).to receive(:write).and_raise(StandardError, 'Cache error')
expect { described_class.write_to_cache(cache_key, value, ttl) }.not_to raise_error
end
end
describe '.read_from_cache' do
let(:cache_key) { 'test_key' }
let(:cached_value) { 'cached_value' }
it 'reads from cache successfully' do
allow(Rails.cache).to receive(:read).with(cache_key).and_return(cached_value)
result = described_class.read_from_cache(cache_key)
expect(result).to eq(cached_value)
end
it 'returns nil when cache read fails' do
allow(Rails.cache).to receive(:read).and_raise(StandardError, 'Cache error')
result = described_class.read_from_cache(cache_key)
expect(result).to be_nil
end
it 'returns nil when key not found' do
allow(Rails.cache).to receive(:read).with(cache_key).and_return(nil)
result = described_class.read_from_cache(cache_key)
expect(result).to be_nil
end
end
describe 'constants' do
it 'defines expected TTL constants' do
expect(described_class::FIELD_EXTRACTION_TTL).to eq(3600)
expect(described_class::FIELD_MAPPING_TTL).to eq(1800)
expect(described_class::MAX_CACHE_ENTRIES).to eq(1000)
end
end
end

@ -0,0 +1,191 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AtsPrefill::FieldExtractor do
describe '.call' do
context 'when ats_fields parameter is present' do
let(:fields) { %w[employee_first_name employee_last_name employee_email] }
let(:encoded_fields) { Base64.urlsafe_encode64(fields.to_json) }
let(:params) { ActionController::Parameters.new(ats_fields: encoded_fields) }
it 'decodes and returns the ATS fields' do
result = described_class.call(params)
expect(result).to eq(fields)
end
it 'caches the result' do
cache_key = AtsPrefill::CacheManager.generate_cache_key('ats_fields', encoded_fields)
allow(AtsPrefill::CacheManager).to receive(:fetch_field_extraction).and_call_original
described_class.call(params)
expect(AtsPrefill::CacheManager).to have_received(:fetch_field_extraction).with(cache_key)
end
it 'returns cached result on subsequent calls' do
cache_key = AtsPrefill::CacheManager.generate_cache_key('ats_fields', encoded_fields)
cached_result = ['cached_field']
allow(AtsPrefill::CacheManager).to receive(:fetch_field_extraction).with(cache_key).and_return(cached_result)
result = described_class.call(params)
expect(result).to eq(cached_result)
end
end
context 'when ats_fields parameter is missing' do
let(:params) { ActionController::Parameters.new({}) }
it 'returns an empty array' do
result = described_class.call(params)
expect(result).to eq([])
end
end
context 'when ats_fields parameter is blank' do
let(:params) { ActionController::Parameters.new(ats_fields: '') }
it 'returns an empty array' do
result = described_class.call(params)
expect(result).to eq([])
end
end
context 'when ats_fields parameter is invalid' do
let(:params) { ActionController::Parameters.new(ats_fields: 'invalid-base64') }
it 'returns an empty array' do
result = described_class.call(params)
expect(result).to eq([])
end
end
context 'when decoded JSON is not an array' do
let(:invalid_data) { { not: 'an array' } }
let(:encoded_invalid) { Base64.urlsafe_encode64(invalid_data.to_json) }
let(:params) { ActionController::Parameters.new(ats_fields: encoded_invalid) }
it 'returns an empty array' do
result = described_class.call(params)
expect(result).to eq([])
end
end
context 'when array contains non-string values' do
let(:mixed_data) { ['employee_first_name', 123, 'employee_email'] }
let(:encoded_mixed) { Base64.urlsafe_encode64(mixed_data.to_json) }
let(:params) { ActionController::Parameters.new(ats_fields: encoded_mixed) }
it 'returns an empty array' do
result = described_class.call(params)
expect(result).to eq([])
end
end
context 'when validating field names' do
it 'accepts all valid field name patterns' do
valid_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(valid_fields.to_json)
params = ActionController::Parameters.new(ats_fields: encoded)
result = described_class.call(params)
expect(result).to eq(valid_fields)
end
it 'rejects invalid field name patterns' do
invalid_fields = %w[
invalid_field
employee
_employee_name
employee_name_
EMPLOYEE_NAME
employee-name
employee.name
malicious_script
admin_password
]
encoded = Base64.urlsafe_encode64(invalid_fields.to_json)
params = ActionController::Parameters.new(ats_fields: encoded)
result = described_class.call(params)
expect(result).to eq([])
end
it 'filters out invalid fields while keeping valid ones' do
mixed_fields = %w[
employee_first_name
invalid_field
manager_lastname
malicious_script
location_name
]
expected_valid = %w[employee_first_name manager_lastname location_name]
encoded = Base64.urlsafe_encode64(mixed_fields.to_json)
params = ActionController::Parameters.new(ats_fields: encoded)
result = described_class.call(params)
expect(result).to eq(expected_valid)
end
end
context 'when handling errors' do
it 'handles JSON parsing errors gracefully' do
invalid_json = Base64.urlsafe_encode64('invalid json')
params = ActionController::Parameters.new(ats_fields: invalid_json)
result = described_class.call(params)
expect(result).to eq([])
end
it 'handles Base64 decoding errors gracefully' do
params = ActionController::Parameters.new(ats_fields: 'invalid-base64!')
result = described_class.call(params)
expect(result).to eq([])
end
end
end
describe 'VALID_FIELD_PATTERN' do
it 'matches expected field patterns' do
valid_patterns = %w[
employee_first_name
manager_last_name
account_company_name
location_street_address
]
expect(valid_patterns).to all(match(described_class::VALID_FIELD_PATTERN))
end
it 'rejects invalid field patterns' do
invalid_patterns = %w[
invalid_field
employee
_employee_name
employee_name_
EMPLOYEE_NAME
employee-name
employee.name
123_field
field_123
]
invalid_patterns.each do |pattern|
expect(pattern).not_to match(described_class::VALID_FIELD_PATTERN)
end
end
end
end

@ -0,0 +1,251 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AtsPrefill::FieldMapper do
let(:template_fields) do
[
{
'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
describe '.call' do
context 'when template_fields is provided' do
it 'returns correct mapping of prefill names to UUIDs' do
result = described_class.call(template_fields)
expect(result).to eq({
'employee_first_name' => 'field-1-uuid',
'employee_last_name' => 'field-2-uuid',
'employee_email' => 'field-3-uuid'
})
end
it 'excludes fields without prefill attributes' do
result = described_class.call(template_fields)
expect(result).not_to have_key('signature')
expect(result.values).not_to include('field-4-uuid')
end
it 'caches the result' do
cache_signature = template_fields
.filter_map do |f|
"#{f['uuid']}:#{f['prefill']}" if f['uuid'].present? && f['prefill'].present?
end
.sort
.join('|')
cache_key = AtsPrefill::CacheManager.generate_cache_key('field_mapping', cache_signature)
allow(AtsPrefill::CacheManager).to receive(:fetch_field_mapping).and_call_original
described_class.call(template_fields)
expect(AtsPrefill::CacheManager).to have_received(:fetch_field_mapping).with(cache_key)
end
it 'returns cached result on subsequent calls' do
cache_signature = template_fields
.filter_map do |f|
"#{f['uuid']}:#{f['prefill']}" if f['uuid'].present? && f['prefill'].present?
end
.sort
.join('|')
cache_key = AtsPrefill::CacheManager.generate_cache_key('field_mapping', cache_signature)
cached_result = { 'cached_field' => 'cached_uuid' }
allow(AtsPrefill::CacheManager).to receive(:fetch_field_mapping).with(cache_key).and_return(cached_result)
result = described_class.call(template_fields)
expect(result).to eq(cached_result)
end
end
context 'when template_fields is nil' do
it 'returns empty hash' do
result = described_class.call(nil)
expect(result).to eq({})
end
end
context 'when template_fields is empty' do
it 'returns empty hash' do
result = described_class.call([])
expect(result).to eq({})
end
end
context 'when fields have missing attributes' do
let(:incomplete_fields) do
[
{
'uuid' => 'field-1-uuid',
'prefill' => 'employee_first_name'
},
{
'uuid' => 'field-2-uuid'
# Missing prefill
},
{
'prefill' => 'employee_last_name'
# Missing uuid
},
{
'uuid' => '',
'prefill' => 'employee_email'
},
{
'uuid' => 'field-5-uuid',
'prefill' => ''
}
]
end
it 'only includes fields with both uuid and prefill present' do
result = described_class.call(incomplete_fields)
expect(result).to eq({
'employee_first_name' => 'field-1-uuid'
})
end
end
context 'with duplicate prefill names' do
let(:duplicate_fields) do
[
{
'uuid' => 'field-1-uuid',
'prefill' => 'employee_name'
},
{
'uuid' => 'field-2-uuid',
'prefill' => 'employee_name'
}
]
end
it 'uses the last occurrence for duplicate prefill names' do
result = described_class.call(duplicate_fields)
expect(result).to eq({
'employee_name' => 'field-2-uuid'
})
end
end
end
describe '.find_field_uuid' do
context 'when template_fields is provided' do
it 'returns the correct UUID for a matching ATS field name' do
uuid = described_class.find_field_uuid('employee_first_name', template_fields)
expect(uuid).to eq('field-1-uuid')
end
it 'returns the correct UUID for another matching ATS field name' do
uuid = described_class.find_field_uuid('employee_email', template_fields)
expect(uuid).to eq('field-3-uuid')
end
it 'returns nil for a non-matching ATS field name' do
uuid = described_class.find_field_uuid('non_existent_field', template_fields)
expect(uuid).to be_nil
end
it 'returns nil for a field without prefill attribute' do
uuid = described_class.find_field_uuid('signature', template_fields)
expect(uuid).to be_nil
end
it 'uses the cached field mapping' do
allow(described_class).to receive(:call).with(template_fields).and_return(
{ 'employee_first_name' => 'field-1-uuid' }
)
uuid = described_class.find_field_uuid('employee_first_name',
template_fields)
expect(uuid).to eq('field-1-uuid')
end
end
context 'when template_fields is nil' do
it 'returns nil' do
uuid = described_class.find_field_uuid('employee_first_name', nil)
expect(uuid).to be_nil
end
end
context 'when template_fields is empty' do
it 'returns nil' do
uuid = described_class.find_field_uuid('employee_first_name', [])
expect(uuid).to be_nil
end
end
context 'when field_name is blank' do
it 'returns nil for nil field_name' do
uuid = described_class.find_field_uuid(nil, template_fields)
expect(uuid).to be_nil
end
it 'returns nil for empty field_name' do
uuid = described_class.find_field_uuid('', template_fields)
expect(uuid).to be_nil
end
end
end
describe 'cache signature generation' do
it 'generates consistent cache signatures' do
signature1 = described_class.send(:build_cache_signature, template_fields)
signature2 = described_class.send(:build_cache_signature, template_fields)
expect(signature1).to eq(signature2)
end
it 'generates different signatures for different field sets' do
different_fields = [
{
'uuid' => 'different-uuid',
'prefill' => 'different_field'
}
]
signature1 = described_class.send(:build_cache_signature, template_fields)
signature2 = described_class.send(:build_cache_signature, different_fields)
expect(signature1).not_to eq(signature2)
end
it 'generates same signature regardless of field order' do
shuffled_fields = template_fields.shuffle
signature1 = described_class.send(:build_cache_signature, template_fields)
signature2 = described_class.send(:build_cache_signature, shuffled_fields)
expect(signature1).to eq(signature2)
end
end
end

@ -0,0 +1,213 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AtsPrefill::ValueMerger do
let(:template_fields) do
[
{
'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
describe '.call' do
let(:submitter_values) do
{
'field-1-uuid' => 'Existing First Name',
'field-4-uuid' => 'Existing Signature'
}
end
let(:ats_values) do
{
'employee_first_name' => 'John',
'employee_last_name' => 'Doe',
'employee_email' => 'john.doe@example.com'
}
end
context 'when template_fields is provided' do
it 'merges ATS values for fields that do not have existing submitter values' do
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result).to include(
'field-1-uuid' => 'Existing First Name', # Should not be overwritten
'field-2-uuid' => 'Doe', # Should be set from ATS
'field-3-uuid' => 'john.doe@example.com', # Should be set from ATS
'field-4-uuid' => 'Existing Signature' # Should remain unchanged
)
end
it 'does not overwrite existing submitter values' do
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result['field-1-uuid']).to eq('Existing First Name')
end
it 'ignores ATS values for fields without matching prefill attributes' do
ats_values_with_unknown = ats_values.merge('unknown_field' => 'Unknown Value')
result = described_class.call(submitter_values, ats_values_with_unknown, template_fields)
expect(result.keys).not_to include('unknown_field')
end
it 'uses FieldMapper to get field mapping' do
expected_mapping = {
'employee_first_name' => 'field-1-uuid',
'employee_last_name' => 'field-2-uuid',
'employee_email' => 'field-3-uuid'
}
allow(AtsPrefill::FieldMapper).to receive(:call).and_return(expected_mapping)
described_class.call(submitter_values, ats_values, template_fields)
expect(AtsPrefill::FieldMapper).to have_received(:call).with(template_fields)
end
end
context 'when template_fields is nil' do
it 'returns original submitter_values unchanged' do
result = described_class.call(submitter_values, ats_values, nil)
expect(result).to eq(submitter_values)
end
end
context 'when ats_values is blank' do
it 'returns original submitter_values for nil ats_values' do
result = described_class.call(submitter_values, nil, template_fields)
expect(result).to eq(submitter_values)
end
it 'returns original submitter_values for empty ats_values' do
result = described_class.call(submitter_values, {}, template_fields)
expect(result).to eq(submitter_values)
end
end
context 'when submitter_values has blank values' do
let(:submitter_values_with_blanks) do
{
'field-1-uuid' => '',
'field-2-uuid' => nil,
'field-4-uuid' => 'Existing Signature'
}
end
it 'fills blank submitter values with ATS values' do
result = described_class.call(submitter_values_with_blanks, ats_values, template_fields)
expect(result).to include(
'field-1-uuid' => 'John', # Should be filled from ATS (was blank)
'field-2-uuid' => 'Doe', # Should be filled from ATS (was nil)
'field-3-uuid' => 'john.doe@example.com', # Should be set from ATS (was missing)
'field-4-uuid' => 'Existing Signature' # Should remain unchanged
)
end
it 'treats empty string as blank' do
submitter_values = { 'field-1-uuid' => '' }
ats_values = { 'employee_first_name' => 'John' }
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result['field-1-uuid']).to eq('John')
end
it 'treats nil as blank' do
submitter_values = { 'field-1-uuid' => nil }
ats_values = { 'employee_first_name' => 'John' }
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result['field-1-uuid']).to eq('John')
end
it 'does not treat false as blank' do
submitter_values = { 'field-1-uuid' => false }
ats_values = { 'employee_first_name' => 'John' }
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result['field-1-uuid']).to be(false)
end
it 'does not treat zero as blank' do
submitter_values = { 'field-1-uuid' => 0 }
ats_values = { 'employee_first_name' => 'John' }
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result['field-1-uuid']).to eq(0)
end
end
context 'when field mapping is empty' do
it 'returns original submitter values when no fields match' do
allow(AtsPrefill::FieldMapper).to receive(:call).and_return({})
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result).to eq(submitter_values)
end
end
context 'with complex scenarios' do
it 'handles multiple ATS values with partial existing submitter values' do
submitter_values = {
'field-1-uuid' => 'Keep This',
'field-2-uuid' => '',
'field-5-uuid' => 'Unrelated Field'
}
ats_values = {
'employee_first_name' => 'Should Not Override',
'employee_last_name' => 'Should Fill',
'employee_email' => 'Should Add',
'unknown_field' => 'Should Ignore'
}
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result).to eq({
'field-1-uuid' => 'Keep This',
'field-2-uuid' => 'Should Fill',
'field-3-uuid' => 'Should Add',
'field-5-uuid' => 'Unrelated Field'
})
end
it 'modifies the original submitter_values hash' do
original_values = submitter_values.dup
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result).to be(submitter_values)
expect(submitter_values).not_to eq(original_values)
end
end
end
end

@ -0,0 +1,175 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AtsPrefill do
let(:template_fields) do
[
{
'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'
}
]
end
describe '.extract_fields' do
let(:params) { ActionController::Parameters.new(ats_fields: 'encoded_data') }
it 'delegates to FieldExtractor' do
expected_result = %w[employee_first_name employee_last_name]
allow(AtsPrefill::FieldExtractor).to receive(:call).with(params).and_return(expected_result)
result = described_class.extract_fields(params)
expect(result).to eq(expected_result)
end
end
describe '.merge_values' do
let(:submitter_values) { { 'field-1-uuid' => 'John' } }
let(:ats_values) { { 'employee_last_name' => 'Doe' } }
it 'delegates to ValueMerger' do
expected_result = { 'field-1-uuid' => 'John', 'field-2-uuid' => 'Doe' }
allow(AtsPrefill::ValueMerger).to receive(:call).with(submitter_values, ats_values,
template_fields).and_return(expected_result)
result = described_class.merge_values(submitter_values, ats_values, template_fields)
expect(result).to eq(expected_result)
end
it 'handles nil template_fields' do
allow(AtsPrefill::ValueMerger).to receive(:call).with(submitter_values, ats_values,
nil).and_return(submitter_values)
result = described_class.merge_values(submitter_values, ats_values, nil)
expect(result).to eq(submitter_values)
end
end
describe '.find_field_uuid' do
let(:field_name) { 'employee_first_name' }
it 'delegates to FieldMapper.find_field_uuid' do
expected_uuid = 'field-1-uuid'
allow(AtsPrefill::FieldMapper).to receive(:find_field_uuid).with(field_name,
template_fields).and_return(expected_uuid)
result = described_class.find_field_uuid(field_name, template_fields)
expect(result).to eq(expected_uuid)
end
it 'returns nil for non-existent field' do
allow(AtsPrefill::FieldMapper).to receive(:find_field_uuid).with('non_existent', template_fields).and_return(nil)
result = described_class.find_field_uuid('non_existent', template_fields)
expect(result).to be_nil
end
end
describe '.build_field_mapping' do
it 'delegates to FieldMapper.call' do
expected_mapping = { 'employee_first_name' => 'field-1-uuid', 'employee_last_name' => 'field-2-uuid' }
allow(AtsPrefill::FieldMapper).to receive(:call).with(template_fields).and_return(expected_mapping)
result = described_class.build_field_mapping(template_fields)
expect(result).to eq(expected_mapping)
end
it 'handles nil template_fields' do
allow(AtsPrefill::FieldMapper).to receive(:call).with(nil).and_return({})
result = described_class.build_field_mapping(nil)
expect(result).to eq({})
end
end
describe '.clear_cache' do
it 'exists as a method for future use' do
expect { described_class.clear_cache }.not_to raise_error
end
it 'returns nil' do
result = described_class.clear_cache
expect(result).to be_nil
end
end
describe 'integration test' do
let(:params) do
fields = %w[employee_first_name employee_last_name]
encoded_fields = Base64.urlsafe_encode64(fields.to_json)
ActionController::Parameters.new(ats_fields: encoded_fields)
end
let(:submitter_values) { { 'field-1-uuid' => 'Existing Name' } }
let(:ats_values) { { 'employee_first_name' => 'John', 'employee_last_name' => 'Doe' } }
it 'works end-to-end with real service objects' do
# Extract fields
extracted_fields = described_class.extract_fields(params)
expect(extracted_fields).to eq(%w[employee_first_name employee_last_name])
# Build field mapping
field_mapping = described_class.build_field_mapping(template_fields)
expect(field_mapping).to eq({
'employee_first_name' => 'field-1-uuid',
'employee_last_name' => 'field-2-uuid'
})
# Find specific field UUID
uuid = described_class.find_field_uuid('employee_first_name', template_fields)
expect(uuid).to eq('field-1-uuid')
# Merge values
merged_values = described_class.merge_values(submitter_values, ats_values, template_fields)
expect(merged_values).to eq({
'field-1-uuid' => 'Existing Name', # Should not be overwritten
'field-2-uuid' => 'Doe' # Should be added from ATS
})
end
end
describe 'module structure' do
it 'includes all expected methods' do
expected_methods = %i[
extract_fields
merge_values
find_field_uuid
build_field_mapping
clear_cache
]
expected_methods.each do |method|
expect(described_class).to respond_to(method)
end
end
it 'is a module with module_function' do
expect(described_class).to be_a(Module)
# Check that methods are available as module methods
expect(described_class).to respond_to(:extract_fields)
expect(described_class.methods).to include(:extract_fields)
end
end
describe 'error handling' do
it 'propagates errors from underlying services' do
allow(AtsPrefill::FieldExtractor).to receive(:call).and_raise(StandardError, 'Test error')
expect { described_class.extract_fields({}) }.to raise_error(StandardError, 'Test error')
end
end
end
Loading…
Cancel
Save