mirror of https://github.com/docusealco/docuseal
parent
24cf871bcc
commit
f180d70474
@ -0,0 +1,96 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'prefill/cache_manager'
|
||||||
|
require_relative 'prefill/field_extractor'
|
||||||
|
require_relative 'prefill/field_mapper'
|
||||||
|
require_relative 'prefill/value_merger'
|
||||||
|
|
||||||
|
# Prefill provides a clean facade for prefill functionality.
|
||||||
|
# This module encapsulates the complexity of extracting, validating, mapping, and merging
|
||||||
|
# prefill 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 prefill integration.
|
||||||
|
#
|
||||||
|
# @example Basic usage
|
||||||
|
# # Extract valid field names from request parameters
|
||||||
|
# field_names = Prefill.extract_fields(params)
|
||||||
|
#
|
||||||
|
# # Merge prefill values with existing submitter values
|
||||||
|
# merged_values = Prefill.merge_values(submitter_values, prefill_values, template_fields)
|
||||||
|
#
|
||||||
|
# # Find specific field UUID by name
|
||||||
|
# field_uuid = Prefill.find_field_uuid('employee_first_name', template_fields)
|
||||||
|
module Prefill
|
||||||
|
# Extracts and validates prefill field names from request parameters
|
||||||
|
#
|
||||||
|
# @param params [ActionController::Parameters] Request parameters containing prefill_fields
|
||||||
|
# @return [Array<String>] Array of valid prefill field names
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Prefill.extract_fields(params)
|
||||||
|
# # => ['employee_first_name', 'employee_email']
|
||||||
|
def extract_fields(params)
|
||||||
|
FieldExtractor.call(params)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Merges prefill values with existing submitter values
|
||||||
|
#
|
||||||
|
# Existing submitter values always take precedence over prefill values to prevent
|
||||||
|
# overwriting user input.
|
||||||
|
#
|
||||||
|
# @param submitter_values [Hash] Existing values entered by submitters
|
||||||
|
# @param prefill_values [Hash] Prefill values from external system
|
||||||
|
# @param template_fields [Array<Hash>, nil] Template field definitions
|
||||||
|
# @return [Hash] Merged values with submitter values taking precedence
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Prefill.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, prefill_values, template_fields = nil)
|
||||||
|
ValueMerger.call(submitter_values, prefill_values, template_fields)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Finds field UUID by matching prefill field name to template field's prefill attribute
|
||||||
|
#
|
||||||
|
# @param field_name [String] Prefill field name to look up
|
||||||
|
# @param template_fields [Array<Hash>, nil] Template field definitions
|
||||||
|
# @return [String, nil] Field UUID if found, nil otherwise
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Prefill.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 prefill field names to field UUIDs
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Prefill.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 prefill-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 Prefill
|
||||||
|
module CacheManager
|
||||||
|
# Cache TTL for prefill 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 Prefill
|
||||||
|
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 prefill field names from Base64-encoded parameters
|
||||||
|
#
|
||||||
|
# This method decodes the prefill_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 prefill field names, empty array if none found or on error
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# # With params[:prefill_fields] = Base64.urlsafe_encode64(['employee_first_name', 'employee_email'].to_json)
|
||||||
|
# Prefill::FieldExtractor.call(params)
|
||||||
|
# # => ['employee_first_name', 'employee_email']
|
||||||
|
def call(params)
|
||||||
|
return [] if params[:prefill_fields].blank?
|
||||||
|
|
||||||
|
cache_key = CacheManager.generate_cache_key('prefill_fields', params[:prefill_fields])
|
||||||
|
|
||||||
|
CacheManager.fetch_field_extraction(cache_key) do
|
||||||
|
extract_and_validate_fields(params[:prefill_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 prefill 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_prefill_field_name?(name) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if a field name matches the valid prefill field pattern
|
||||||
|
#
|
||||||
|
# @param name [String] Field name to validate
|
||||||
|
# @return [Boolean] True if field name is valid, false otherwise
|
||||||
|
def valid_prefill_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_prefill_field_name?
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Prefill
|
||||||
|
module FieldMapper
|
||||||
|
# Creates optimized mapping between prefill field names and template field UUIDs
|
||||||
|
#
|
||||||
|
# Creates a hash mapping prefill 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 prefill field names to field UUIDs
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# template_fields = [
|
||||||
|
# { 'uuid' => 'field-1', 'prefill' => 'employee_first_name' },
|
||||||
|
# { 'uuid' => 'field-2', 'prefill' => 'employee_last_name' }
|
||||||
|
# ]
|
||||||
|
# Prefill::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 prefill 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] Prefill 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 prefill 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 Prefill
|
||||||
|
module ValueMerger
|
||||||
|
# Merges prefill values with existing submitter values
|
||||||
|
#
|
||||||
|
# This method combines externally-provided prefill values with values already entered by submitters.
|
||||||
|
# Existing submitter values always take precedence over prefill 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 prefill_values [Hash] Prefill values from external system, keyed by prefill 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 prefill values
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# submitter_values = { 'field-uuid-1' => 'John' }
|
||||||
|
# prefill_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' }
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# Prefill::ValueMerger.call(submitter_values, prefill_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, prefill_values, template_fields = nil)
|
||||||
|
return submitter_values if prefill_values.blank?
|
||||||
|
|
||||||
|
# Build optimized lookup cache for better performance with large field sets
|
||||||
|
field_mapping = FieldMapper.call(template_fields)
|
||||||
|
|
||||||
|
merge_values(submitter_values, prefill_values, field_mapping)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Merges prefill values into submitter values for fields that are blank
|
||||||
|
#
|
||||||
|
# @param submitter_values [Hash] Current submitter field values
|
||||||
|
# @param prefill_values [Hash] Prefill field values to merge
|
||||||
|
# @param field_mapping [Hash] Mapping of prefill field names to template field UUIDs
|
||||||
|
# @return [Hash] Updated submitter values
|
||||||
|
def merge_values(submitter_values, prefill_values, field_mapping)
|
||||||
|
return submitter_values if prefill_values.blank? || field_mapping.blank?
|
||||||
|
|
||||||
|
prefill_values.each do |prefill_field_name, prefill_value|
|
||||||
|
field_uuid = field_mapping[prefill_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] = prefill_value if current_value.nil? || current_value == ''
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter_values
|
||||||
|
end
|
||||||
|
|
||||||
|
module_function :call, :merge_values
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,233 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
prefill_fields: Base64.urlsafe_encode64(%w[employee_first_name employee_last_name employee_email].to_json),
|
||||||
|
prefill_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_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 prefill_values parameter contains invalid Base64' do
|
||||||
|
let(:test_params) do
|
||||||
|
{
|
||||||
|
prefill_fields: Base64.urlsafe_encode64(['employee_first_name'].to_json),
|
||||||
|
prefill_values: 'invalid-base64!'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles Base64 decoding errors gracefully' do
|
||||||
|
result = controller.send(:fetch_prefill_values_if_available)
|
||||||
|
expect(result).to eq({})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when prefill_values parameter contains valid Base64 but invalid JSON' do
|
||||||
|
let(:test_params) do
|
||||||
|
{
|
||||||
|
prefill_fields: Base64.urlsafe_encode64(['employee_first_name'].to_json),
|
||||||
|
prefill_values: Base64.urlsafe_encode64('invalid json')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles JSON parsing errors gracefully' do
|
||||||
|
result = controller.send(:fetch_prefill_values_if_available)
|
||||||
|
expect(result).to eq({})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when prefill_values parameter contains valid JSON but wrong data type' do
|
||||||
|
let(:test_params) do
|
||||||
|
{
|
||||||
|
prefill_fields: Base64.urlsafe_encode64(['employee_first_name'].to_json),
|
||||||
|
prefill_values: Base64.urlsafe_encode64('["not", "a", "hash"]')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles invalid data type gracefully' do
|
||||||
|
result = controller.send(:fetch_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_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' }
|
||||||
|
prefill_values = { 'employee_first_name' => 'ATS John', 'employee_last_name' => 'ATS Smith' }
|
||||||
|
|
||||||
|
result = merge_prefill_values(existing_values, prefill_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' }
|
||||||
|
prefill_values = {}
|
||||||
|
|
||||||
|
result = merge_prefill_values(existing_values, prefill_values, template_fields)
|
||||||
|
|
||||||
|
expect(result).to eq({
|
||||||
|
'field-1-uuid' => 'Existing John'
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles missing template fields gracefully' do
|
||||||
|
existing_values = {}
|
||||||
|
prefill_values = { 'nonexistent_field' => 'Some Value' }
|
||||||
|
|
||||||
|
result = merge_prefill_values(existing_values, prefill_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
|
||||||
|
prefill_fields_data = %w[employee_first_name employee_last_name employee_email]
|
||||||
|
prefill_values_data = {
|
||||||
|
'employee_first_name' => 'John',
|
||||||
|
'employee_last_name' => 'Smith',
|
||||||
|
'employee_email' => 'john.smith@company.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded_fields = Base64.urlsafe_encode64(prefill_fields_data.to_json)
|
||||||
|
encoded_values = Base64.urlsafe_encode64(prefill_values_data.to_json)
|
||||||
|
|
||||||
|
params = ActionController::Parameters.new({
|
||||||
|
prefill_fields: encoded_fields,
|
||||||
|
prefill_values: encoded_values
|
||||||
|
})
|
||||||
|
|
||||||
|
allow(controller).to receive(:params).and_return(params)
|
||||||
|
|
||||||
|
prefill_values = controller.send(:fetch_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_prefill_values(existing_submitter_values, prefill_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
|
||||||
@ -0,0 +1,148 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Prefill::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 Prefill::FieldExtractor do
|
||||||
|
describe '.call' do
|
||||||
|
context 'when prefill_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(prefill_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 = Prefill::CacheManager.generate_cache_key('prefill_fields', encoded_fields)
|
||||||
|
|
||||||
|
allow(Prefill::CacheManager).to receive(:fetch_field_extraction).and_call_original
|
||||||
|
|
||||||
|
described_class.call(params)
|
||||||
|
|
||||||
|
expect(Prefill::CacheManager).to have_received(:fetch_field_extraction).with(cache_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns cached result on subsequent calls' do
|
||||||
|
cache_key = Prefill::CacheManager.generate_cache_key('prefill_fields', encoded_fields)
|
||||||
|
cached_result = ['cached_field']
|
||||||
|
|
||||||
|
allow(Prefill::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 prefill_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 prefill_fields parameter is blank' do
|
||||||
|
let(:params) { ActionController::Parameters.new(prefill_fields: '') }
|
||||||
|
|
||||||
|
it 'returns an empty array' do
|
||||||
|
result = described_class.call(params)
|
||||||
|
expect(result).to eq([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when prefill_fields parameter is invalid' do
|
||||||
|
let(:params) { ActionController::Parameters.new(prefill_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(prefill_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(prefill_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(prefill_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(prefill_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(prefill_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(prefill_fields: invalid_json)
|
||||||
|
|
||||||
|
result = described_class.call(params)
|
||||||
|
expect(result).to eq([])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles Base64 decoding errors gracefully' do
|
||||||
|
params = ActionController::Parameters.new(prefill_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 Prefill::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 = Prefill::CacheManager.generate_cache_key('field_mapping', cache_signature)
|
||||||
|
|
||||||
|
allow(Prefill::CacheManager).to receive(:fetch_field_mapping).and_call_original
|
||||||
|
|
||||||
|
described_class.call(template_fields)
|
||||||
|
|
||||||
|
expect(Prefill::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 = Prefill::CacheManager.generate_cache_key('field_mapping', cache_signature)
|
||||||
|
cached_result = { 'cached_field' => 'cached_uuid' }
|
||||||
|
|
||||||
|
allow(Prefill::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 Prefill::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(:prefill_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, prefill_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, prefill_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
|
||||||
|
prefill_values_with_unknown = prefill_values.merge('unknown_field' => 'Unknown Value')
|
||||||
|
|
||||||
|
result = described_class.call(submitter_values, prefill_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(Prefill::FieldMapper).to receive(:call).and_return(expected_mapping)
|
||||||
|
|
||||||
|
described_class.call(submitter_values, prefill_values, template_fields)
|
||||||
|
|
||||||
|
expect(Prefill::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, prefill_values, nil)
|
||||||
|
expect(result).to eq(submitter_values)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when prefill_values is blank' do
|
||||||
|
it 'returns original submitter_values for nil prefill_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 prefill_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, prefill_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' => '' }
|
||||||
|
prefill_values = { 'employee_first_name' => 'John' }
|
||||||
|
|
||||||
|
result = described_class.call(submitter_values, prefill_values, template_fields)
|
||||||
|
|
||||||
|
expect(result['field-1-uuid']).to eq('John')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'treats nil as blank' do
|
||||||
|
submitter_values = { 'field-1-uuid' => nil }
|
||||||
|
prefill_values = { 'employee_first_name' => 'John' }
|
||||||
|
|
||||||
|
result = described_class.call(submitter_values, prefill_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 }
|
||||||
|
prefill_values = { 'employee_first_name' => 'John' }
|
||||||
|
|
||||||
|
result = described_class.call(submitter_values, prefill_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 }
|
||||||
|
prefill_values = { 'employee_first_name' => 'John' }
|
||||||
|
|
||||||
|
result = described_class.call(submitter_values, prefill_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(Prefill::FieldMapper).to receive(:call).and_return({})
|
||||||
|
|
||||||
|
result = described_class.call(submitter_values, prefill_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'
|
||||||
|
}
|
||||||
|
|
||||||
|
prefill_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, prefill_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, prefill_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 Prefill 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(prefill_fields: 'encoded_data') }
|
||||||
|
|
||||||
|
it 'delegates to FieldExtractor' do
|
||||||
|
expected_result = %w[employee_first_name employee_last_name]
|
||||||
|
|
||||||
|
allow(Prefill::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(:prefill_values) { { 'employee_last_name' => 'Doe' } }
|
||||||
|
|
||||||
|
it 'delegates to ValueMerger' do
|
||||||
|
expected_result = { 'field-1-uuid' => 'John', 'field-2-uuid' => 'Doe' }
|
||||||
|
|
||||||
|
allow(Prefill::ValueMerger).to receive(:call).with(submitter_values, prefill_values,
|
||||||
|
template_fields).and_return(expected_result)
|
||||||
|
|
||||||
|
result = described_class.merge_values(submitter_values, prefill_values, template_fields)
|
||||||
|
expect(result).to eq(expected_result)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles nil template_fields' do
|
||||||
|
allow(Prefill::ValueMerger).to receive(:call).with(submitter_values, prefill_values,
|
||||||
|
nil).and_return(submitter_values)
|
||||||
|
|
||||||
|
result = described_class.merge_values(submitter_values, prefill_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(Prefill::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(Prefill::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(Prefill::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(Prefill::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(prefill_fields: encoded_fields)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:submitter_values) { { 'field-1-uuid' => 'Existing Name' } }
|
||||||
|
let(:prefill_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, prefill_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 prefill
|
||||||
|
})
|
||||||
|
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(Prefill::FieldExtractor).to receive(:call).and_raise(StandardError, 'Test error')
|
||||||
|
|
||||||
|
expect { described_class.extract_fields({}) }.to raise_error(StandardError, 'Test error')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in new issue