diff --git a/config/locales/en.yml b/config/locales/en.yml index 5b517b6f..3b07fc40 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -62,6 +62,8 @@ en: other: Unsupported MIME types detected in %{file_names}. multiple_channel_integrations: Specifying multiple channel integrations in requirements.json is not supported. + custom_object_key_mismatch: 'Object key mismatch in custom_objects_v2: object "%{object_name}" has key field "%{actual_key}" but should be "%{expected_key}"' + custom_object_trigger_key_mismatch: 'Trigger key mismatch in object_triggers: trigger "%{trigger_name}" has key field "%{actual_key}" but should be "%{expected_key}"' oauth_parameter_required: "Please upgrade to our new oauth format. Learn more: %{link}" invalid_cr_schema_keys: one: 'Custom resources schema contains an invalid key: %{invalid_keys}' diff --git a/lib/zendesk_apps_support/app_requirement.rb b/lib/zendesk_apps_support/app_requirement.rb index 06438417..5d1eb2b8 100644 --- a/lib/zendesk_apps_support/app_requirement.rb +++ b/lib/zendesk_apps_support/app_requirement.rb @@ -4,9 +4,10 @@ module ZendeskAppsSupport class AppRequirement WEBHOOKS_KEY = 'webhooks' CUSTOM_OBJECTS_KEY = 'custom_objects' + CUSTOM_OBJECTS_VERSION_2_KEY = 'custom_objects_v2' CUSTOM_OBJECTS_TYPE_KEY = 'custom_object_types' CUSTOM_OBJECTS_RELATIONSHIP_TYPE_KEY = 'custom_object_relationship_types' TYPES = %w[automations channel_integrations custom_objects macros targets views ticket_fields - triggers user_fields organization_fields webhooks].freeze + triggers user_fields organization_fields webhooks custom_objects_v2].freeze end end diff --git a/lib/zendesk_apps_support/validations/requirements.rb b/lib/zendesk_apps_support/validations/requirements.rb index d513945c..18c522b9 100644 --- a/lib/zendesk_apps_support/validations/requirements.rb +++ b/lib/zendesk_apps_support/validations/requirements.rb @@ -32,6 +32,7 @@ def call(package) errors << invalid_webhooks(requirements) errors << invalid_target_types(requirements) errors << missing_required_fields(requirements) + errors << invalid_custom_objects_v2(requirements) errors.flatten! errors.compact! end @@ -48,7 +49,7 @@ def supports_requirements(package) def missing_required_fields(requirements) [].tap do |errors| requirements.each do |requirement_type, requirement| - next if %w[channel_integrations custom_objects webhooks].include? requirement_type + next if %w[channel_integrations custom_objects webhooks custom_objects_v2].include? requirement_type requirement.each do |identifier, fields| next if fields.nil? || fields.include?('title') errors << ValidationError.new(:missing_required_fields, @@ -131,6 +132,137 @@ def validate_webhook_keys(identifier, requirement) end end + def invalid_custom_objects_v2(requirements) + custom_objects_v2_requirements = requirements[AppRequirement::CUSTOM_OBJECTS_VERSION_2_KEY] + return if custom_objects_v2_requirements.nil? + + validate_custom_objects_v2_keys(custom_objects_v2_requirements) + end + + def validate_custom_objects_v2_keys(custom_objects_v2_requirements) + errors = [] + + # Check if objects hash exists + objects = custom_objects_v2_requirements['objects'] + return if objects.nil? + + required_object_keys = %w[key include_in_list_view title title_pluralized] + + objects.each do |object_key, object| + missing_keys = required_object_keys - object.keys + + missing_keys.each do |key| + errors << ValidationError.new(:missing_required_fields, + field: key, + identifier: "#{AppRequirement::CUSTOM_OBJECTS_VERSION_2_KEY} objects.#{object_key}") + end + + # Validate that object_key matches the 'key' field inside the object + if object['key'] && object['key'] != object_key + errors << ValidationError.new(:custom_object_key_mismatch, + object_name: object_key, + expected_key: object_key, + actual_key: object['key']) + end + end + + # Validate object_triggers if present + object_triggers = custom_objects_v2_requirements['object_triggers'] + errors.concat(validate_object_triggers(object_triggers, objects)) if object_triggers + + errors + end + + def validate_object_triggers(object_triggers, objects) + errors = [] + return errors if object_triggers.nil? || objects.nil? + + # Get all valid object keys + valid_object_keys = objects.keys + # Get all valid field names from objects + valid_fields = objects.flat_map { |_, obj| (obj['fields'] || []).map { |field| field['key'] } }.compact.uniq + + object_triggers.each do |trigger_key, trigger| + trigger_identifier = "object_triggers.#{trigger_key}" + + # Validate required keys for trigger (replaced 'key' with 'object_key', kept conditions) + required_trigger_keys = %w[object_key title conditions actions] + missing_keys = required_trigger_keys - trigger.keys + + missing_keys.each do |key| + errors << ValidationError.new(:missing_required_fields, + field: key, + identifier: trigger_identifier) + end + + # Validate that object_key matches one of the valid object keys + if trigger['object_key'] && !valid_object_keys.include?(trigger['object_key']) + errors << ValidationError.new(:missing_required_fields, + field: "object_key '#{trigger['object_key']}' (must reference valid object: #{valid_object_keys.join(', ')})", + identifier: trigger_identifier) + end + + # Validate actions array + if trigger['actions'] + trigger['actions'].each_with_index do |action, action_index| + action_identifier = "#{trigger_identifier}.actions[#{action_index}]" + + # Each action must have 'field' and 'value' + required_action_keys = %w[field value] + missing_action_keys = required_action_keys - action.keys + + missing_action_keys.each do |key| + errors << ValidationError.new(:missing_required_fields, + field: key, + identifier: action_identifier) + end + end + end + + # Validate conditions + errors.concat(validate_trigger_conditions(trigger['conditions'], valid_fields, trigger_identifier)) if trigger['conditions'] + end + + errors + end + + def validate_trigger_conditions(conditions, valid_fields, trigger_identifier) + errors = [] + + # Conditions can have 'all' and/or 'any' keys + %w[all any].each do |condition_type| + next unless conditions[condition_type] + + unless conditions[condition_type].is_a?(Array) + errors << ValidationError.new(:missing_required_fields, + field: "conditions.#{condition_type} (must be array)", + identifier: trigger_identifier) + next + end + + conditions[condition_type].each_with_index do |condition, condition_index| + condition_identifier = "#{trigger_identifier}.conditions.#{condition_type}[#{condition_index}]" + + # Each condition must have 'field' + unless condition.key?('field') + errors << ValidationError.new(:missing_required_fields, + field: 'field', + identifier: condition_identifier) + else + # Validate that field exists in objects - use existing error format + field_name = condition['field'] + unless valid_fields.include?(field_name) + errors << ValidationError.new(:missing_required_fields, + field: "field '#{field_name}' (must reference valid object field: #{valid_fields.join(', ')})", + identifier: condition_identifier) + end + end + end + end + + errors + end + def invalid_custom_objects(requirements) custom_objects = requirements[AppRequirement::CUSTOM_OBJECTS_KEY] return if custom_objects.nil? diff --git a/spec/validations/requirements_spec.rb b/spec/validations/requirements_spec.rb index 025cbab1..1ef95d46 100644 --- a/spec/validations/requirements_spec.rb +++ b/spec/validations/requirements_spec.rb @@ -361,4 +361,728 @@ end end end + + context 'custom objects v2 requirements validations' do + context 'there is a valid custom objects v2 schema defined' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects' + } + } + } + ) + end + + it 'does not return an error' do + expect(errors).to be_empty + end + end + + context 'there is a valid custom objects v2 schema with object_triggers' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects', + 'fields' => [ + { 'key' => 'status', 'type' => 'dropdown' }, + { 'key' => 'priority', 'type' => 'text' } + ] + } + }, + 'object_triggers' => { + 'my_object_trigger' => { + 'object_key' => 'my_custom_object', + 'title' => 'My Object Trigger', + 'conditions' => { + 'all' => [ + { 'field' => 'status' } + ] + }, + 'actions' => [ + { 'field' => 'priority', 'value' => 'high' } + ] + } + } + } + ) + end + + it 'does not return an error' do + expect(errors).to be_empty + end + end + + context 'custom objects v2 with multiple valid objects' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'first_object' => { + 'key' => 'first_object', + 'include_in_list_view' => true, + 'title' => 'First Object', + 'title_pluralized' => 'First Objects' + }, + 'second_object' => { + 'key' => 'second_object', + 'include_in_list_view' => false, + 'title' => 'Second Object', + 'title_pluralized' => 'Second Objects' + } + } + } + ) + end + + it 'does not return an error' do + expect(errors).to be_empty + end + end + + context 'custom objects v2 object is missing required key field' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects' + } + } + } + ) + end + + it 'creates an error for missing key field' do + expect(errors.first.key).to eq(:missing_required_fields) + expect(errors.first.data).to eq(field: 'key', identifier: 'custom_objects_v2 objects.my_custom_object') + end + end + + context 'custom objects v2 object is missing required include_in_list_view field' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects' + } + } + } + ) + end + + it 'creates an error for missing include_in_list_view field' do + expect(errors.first.key).to eq(:missing_required_fields) + expect(errors.first.data).to eq(field: 'include_in_list_view', identifier: 'custom_objects_v2 objects.my_custom_object') + end + end + + context 'custom objects v2 object is missing required title field' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title_pluralized' => 'My Custom Objects' + } + } + } + ) + end + + it 'creates an error for missing title field' do + expect(errors.first.key).to eq(:missing_required_fields) + expect(errors.first.data).to eq(field: 'title', identifier: 'custom_objects_v2 objects.my_custom_object') + end + end + + context 'custom objects v2 object is missing required title_pluralized field' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object' + } + } + } + ) + end + + it 'creates an error for missing title_pluralized field' do + expect(errors.first.key).to eq(:missing_required_fields) + expect(errors.first.data).to eq(field: 'title_pluralized', identifier: 'custom_objects_v2 objects.my_custom_object') + end + end + + context 'custom objects v2 object is missing multiple required fields' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'incomplete_object' => { + 'include_in_list_view' => true + } + } + } + ) + end + let(:required_keys) { ['key', 'title', 'title_pluralized'] } + + it 'creates errors for all missing fields' do + errors.each do |error| + expect(error.key).to eq(:missing_required_fields) + expect(required_keys).to include(error.data[:field]) + expect(error.data[:identifier]).to eq('custom_objects_v2 objects.incomplete_object') + end + expect(errors.count).to eq(required_keys.count) + end + end + + context 'custom objects v2 object is completely empty' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'empty_object' => {} + } + } + ) + end + let(:required_keys) { ['key', 'include_in_list_view', 'title', 'title_pluralized'] } + + it 'creates errors for all required fields' do + errors.each do |error| + expect(error.key).to eq(:missing_required_fields) + expect(required_keys).to include(error.data[:field]) + expect(error.data[:identifier]).to eq('custom_objects_v2 objects.empty_object') + end + expect(errors.count).to eq(required_keys.count) + end + end + + context 'multiple custom objects v2 objects with missing fields' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'first_incomplete' => { + 'include_in_list_view' => true + }, + 'second_incomplete' => { + 'key' => 'second_incomplete' + } + } + } + ) + end + + it 'creates errors for missing fields in both objects' do + # First object missing: key, title, title_pluralized + first_object_errors = errors.select { |e| e.data[:identifier] == 'custom_objects_v2 objects.first_incomplete' } + expect(first_object_errors.count).to eq(3) + first_object_missing_fields = first_object_errors.map { |e| e.data[:field] } + expect(first_object_missing_fields).to include('key', 'title', 'title_pluralized') + + # Second object missing: include_in_list_view, title, title_pluralized + second_object_errors = errors.select { |e| e.data[:identifier] == 'custom_objects_v2 objects.second_incomplete' } + expect(second_object_errors.count).to eq(3) + second_object_missing_fields = second_object_errors.map { |e| e.data[:field] } + expect(second_object_missing_fields).to include('include_in_list_view', 'title', 'title_pluralized') + + expect(errors.count).to eq(6) + end + end + + context 'custom objects v2 schema has no objects array' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => {} + ) + end + + it 'does not create any validation errors' do + expect(errors).to be_empty + end + end + + context 'custom objects v2 schema has empty objects hash' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => {} + } + ) + end + + it 'does not create any validation errors' do + expect(errors).to be_empty + end + end + + context 'no custom objects v2 requirements are present' do + let(:requirements_string) do + JSON.generate( + 'targets' => { + 'my_target' => { + 'title' => 'My Target', + 'type' => 'email_target' + } + } + ) + end + + it 'does not create any custom objects v2 validation errors' do + expect(errors).to be_empty + end + end + + context 'object_triggers validation' do + context 'object_triggers missing required object_key field' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects', + 'fields' => [ + { 'key' => 'status', 'type' => 'dropdown' } + ] + } + }, + 'object_triggers' => { + 'my_trigger' => { + 'title' => 'My Trigger', + 'conditions' => { + 'all' => [ + { 'field' => 'status' } + ] + }, + 'actions' => [ + { 'field' => 'status', 'value' => 'open' } + ] + } + } + } + ) + end + + it 'creates an error for missing object_key field' do + expect(errors.first.key).to eq(:missing_required_fields) + expect(errors.first.data).to eq(field: 'object_key', identifier: 'object_triggers.my_trigger') + end + end + + context 'object_triggers missing required title field' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects', + 'fields' => [ + { 'key' => 'status', 'type' => 'dropdown' } + ] + } + }, + 'object_triggers' => { + 'my_trigger' => { + 'object_key' => 'my_custom_object', + 'conditions' => { + 'all' => [ + { 'field' => 'status' } + ] + }, + 'actions' => [ + { 'field' => 'status', 'value' => 'open' } + ] + } + } + } + ) + end + + it 'creates an error for missing title field' do + expect(errors.first.key).to eq(:missing_required_fields) + expect(errors.first.data).to eq(field: 'title', identifier: 'object_triggers.my_trigger') + end + end + + context 'object_triggers missing required conditions field' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects' + } + }, + 'object_triggers' => { + 'my_trigger' => { + 'object_key' => 'my_custom_object', + 'title' => 'My Trigger', + 'actions' => [ + { 'field' => 'status', 'value' => 'open' } + ] + } + } + } + ) + end + + it 'creates an error for missing conditions field' do + expect(errors.first.key).to eq(:missing_required_fields) + expect(errors.first.data).to eq(field: 'conditions', identifier: 'object_triggers.my_trigger') + end + end + + context 'object_triggers missing required actions field' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects', + 'fields' => [ + { 'key' => 'status', 'type' => 'dropdown' } + ] + } + }, + 'object_triggers' => { + 'my_trigger' => { + 'object_key' => 'my_custom_object', + 'title' => 'My Trigger', + 'conditions' => { + 'all' => [ + { 'field' => 'status' } + ] + } + } + } + } + ) + end + + it 'creates an error for missing actions field' do + expect(errors.first.key).to eq(:missing_required_fields) + expect(errors.first.data).to eq(field: 'actions', identifier: 'object_triggers.my_trigger') + end + end + + context 'object_triggers with action missing field key' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects' + } + }, + 'object_triggers' => { + 'my_trigger' => { + 'object_key' => 'my_custom_object', + 'title' => 'My Trigger', + 'conditions' => { + 'all' => [] + }, + 'actions' => [ + { 'value' => 'open' } + ] + } + } + } + ) + end + + it 'creates an error for action missing field key' do + error = errors.find { |e| e.data[:identifier] == 'object_triggers.my_trigger.actions[0]' && e.data[:field] == 'field' } + expect(error).not_to be_nil + expect(error.key).to eq(:missing_required_fields) + end + end + + context 'object_triggers with action missing value key' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects' + } + }, + 'object_triggers' => { + 'my_trigger' => { + 'object_key' => 'my_custom_object', + 'title' => 'My Trigger', + 'conditions' => { + 'all' => [] + }, + 'actions' => [ + { 'field' => 'status' } + ] + } + } + } + ) + end + + it 'creates an error for action missing value key' do + error = errors.find { |e| e.data[:identifier] == 'object_triggers.my_trigger.actions[0]' && e.data[:field] == 'value' } + expect(error).not_to be_nil + expect(error.key).to eq(:missing_required_fields) + end + end + + context 'object_triggers with conditions.all not being an array' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects' + } + }, + 'object_triggers' => { + 'my_trigger' => { + 'object_key' => 'my_custom_object', + 'title' => 'My Trigger', + 'conditions' => { + 'all' => 'not_an_array' + }, + 'actions' => [] + } + } + } + ) + end + + it 'creates an error for conditions.all not being an array' do + error = errors.find { |e| e.data[:field] == 'conditions.all (must be array)' } + expect(error).not_to be_nil + expect(error.key).to eq(:missing_required_fields) + expect(error.data[:identifier]).to eq('object_triggers.my_trigger') + end + end + + context 'object_triggers with condition missing field key' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects', + 'fields' => [ + { 'key' => 'status', 'type' => 'dropdown' } + ] + } + }, + 'object_triggers' => { + 'my_trigger' => { + 'object_key' => 'my_custom_object', + 'title' => 'My Trigger', + 'conditions' => { + 'all' => [ + { 'operator' => 'equals' } + ] + }, + 'actions' => [] + } + } + } + ) + end + + it 'creates an error for condition missing field key' do + error = errors.find { |e| e.data[:identifier] == 'object_triggers.my_trigger.conditions.all[0]' && e.data[:field] == 'field' } + expect(error).not_to be_nil + expect(error.key).to eq(:missing_required_fields) + end + end + + context 'object_triggers with condition field referencing non-existent object field' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects', + 'fields' => [ + { 'key' => 'status', 'type' => 'dropdown' } + ] + } + }, + 'object_triggers' => { + 'my_trigger' => { + 'object_key' => 'my_custom_object', + 'title' => 'My Trigger', + 'conditions' => { + 'all' => [ + { 'field' => 'nonexistent_field' } + ] + }, + 'actions' => [ + { 'field' => 'status', 'value' => 'open' } + ] + } + } + } + ) + end + + it 'creates an error for invalid field reference' do + error = errors.find { |e| e.data[:identifier] == 'object_triggers.my_trigger.conditions.all[0]' } + expect(error).not_to be_nil + expect(error.key).to eq(:missing_required_fields) + expect(error.data[:field]).to include('nonexistent_field') + expect(error.data[:field]).to include('status') + end + end + + context 'object_triggers with invalid object_key reference' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'my_custom_object', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects', + 'fields' => [ + { 'key' => 'status', 'type' => 'dropdown' } + ] + } + }, + 'object_triggers' => { + 'my_trigger' => { + 'object_key' => 'nonexistent_object', + 'title' => 'My Trigger', + 'conditions' => { + 'all' => [ + { 'field' => 'status' } + ] + }, + 'actions' => [ + { 'field' => 'status', 'value' => 'open' } + ] + } + } + } + ) + end + + it 'creates an error for invalid object_key reference' do + error = errors.find { |e| e.data[:identifier] == 'object_triggers.my_trigger' && e.data[:field].include?('object_key') } + expect(error).not_to be_nil + expect(error.key).to eq(:missing_required_fields) + expect(error.data[:field]).to include('nonexistent_object') + expect(error.data[:field]).to include('my_custom_object') + end + end + end + + context 'custom object key mismatch validation' do + context 'object key does not match its internal key field' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'my_custom_object' => { + 'key' => 'different_key', + 'include_in_list_view' => true, + 'title' => 'My Custom Object', + 'title_pluralized' => 'My Custom Objects' + } + } + } + ) + end + + it 'creates an error for key mismatch' do + expect(errors.first.key).to eq(:custom_object_key_mismatch) + expect(errors.first.data).to eq(object_name: 'my_custom_object', expected_key: 'my_custom_object', actual_key: 'different_key') + end + end + + context 'multiple objects with key mismatches' do + let(:requirements_string) do + JSON.generate( + 'custom_objects_v2' => { + 'objects' => { + 'first_object' => { + 'key' => 'wrong_first_key', + 'include_in_list_view' => true, + 'title' => 'First Object', + 'title_pluralized' => 'First Objects' + }, + 'second_object' => { + 'key' => 'wrong_second_key', + 'include_in_list_view' => false, + 'title' => 'Second Object', + 'title_pluralized' => 'Second Objects' + } + } + } + ) + end + + it 'creates errors for both key mismatches' do + mismatch_errors = errors.select { |e| e.key == :custom_object_key_mismatch } + expect(mismatch_errors.count).to eq(2) + + first_error = mismatch_errors.find { |e| e.data[:object_name] == 'first_object' } + expect(first_error.data).to eq(object_name: 'first_object', expected_key: 'first_object', actual_key: 'wrong_first_key') + + second_error = mismatch_errors.find { |e| e.data[:object_name] == 'second_object' } + expect(second_error.data).to eq(object_name: 'second_object', expected_key: 'second_object', actual_key: 'wrong_second_key') + end + end + end + end end