From 4d209b805b776cc22fc2cb70b7b4f3f7cf20c032 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Mon, 19 Dec 2016 09:59:54 +0100 Subject: [PATCH] - Major refactoring of Zendesk import logic including introduction of RSpec tests. - Fixed issue #487: No login possible after zendesk import. --- .../app/views/import/zendesk.jst.eco | 4 +- lib/import/base_factory.rb | 22 +- lib/import/factory.rb | 9 +- lib/import/transaction_factory.rb | 6 +- lib/import/zendesk.rb | 1061 +---------------- lib/import/zendesk/async.rb | 65 + lib/import/zendesk/base_factory.rb | 16 + lib/import/zendesk/group.rb | 21 + lib/import/zendesk/group_factory.rb | 8 + lib/import/zendesk/helper.rb | 19 + lib/import/zendesk/import_stats.rb | 61 + lib/import/zendesk/local_id_mapper_hook.rb | 25 + lib/import/zendesk/object_attribute.rb | 73 ++ .../zendesk/object_attribute/checkbox.rb | 23 + lib/import/zendesk/object_attribute/date.rb | 20 + .../zendesk/object_attribute/decimal.rb | 8 + .../zendesk/object_attribute/dropdown.rb | 8 + .../zendesk/object_attribute/integer.rb | 25 + lib/import/zendesk/object_attribute/regexp.rb | 26 + lib/import/zendesk/object_attribute/select.rb | 28 + lib/import/zendesk/object_attribute/tagger.rb | 8 + lib/import/zendesk/object_attribute/text.rb | 20 + .../zendesk/object_attribute/textarea.rb | 20 + lib/import/zendesk/object_field.rb | 39 + lib/import/zendesk/organization.rb | 34 + lib/import/zendesk/organization_factory.rb | 10 + lib/import/zendesk/organization_field.rb | 6 + .../zendesk/organization_field_factory.rb | 8 + lib/import/zendesk/priority.rb | 32 + lib/import/zendesk/requester.rb | 39 + lib/import/zendesk/state.rb | 29 + lib/import/zendesk/ticket.rb | 73 ++ lib/import/zendesk/ticket/comment.rb | 71 ++ .../zendesk/ticket/comment/attachment.rb | 43 + .../ticket/comment/attachment_factory.rb | 20 + lib/import/zendesk/ticket/comment/sender.rb | 49 + lib/import/zendesk/ticket/comment/type.rb | 60 + lib/import/zendesk/ticket/comment_factory.rb | 9 + .../zendesk/ticket/sub_object_factory.rb | 21 + lib/import/zendesk/ticket/tag.rb | 16 + lib/import/zendesk/ticket/tag_factory.rb | 9 + lib/import/zendesk/ticket_factory.rb | 7 + lib/import/zendesk/ticket_field.rb | 12 + lib/import/zendesk/ticket_field_factory.rb | 33 + lib/import/zendesk/user.rb | 75 ++ lib/import/zendesk/user/group.rb | 51 + lib/import/zendesk/user/role.rb | 62 + lib/import/zendesk/user_factory.rb | 8 + lib/import/zendesk/user_field.rb | 6 + lib/import/zendesk/user_field_factory.rb | 8 + spec/import/{otrs => }/async_examples.rb | 2 +- spec/import/base_factory_examples.rb | 5 +- .../{otrs => }/import_stats_examples.rb | 2 +- spec/import/otrs/priority_factory_spec.rb | 9 + spec/import/otrs_spec.rb | 8 +- spec/import/zendesk/base_factory_examples.rb | 23 + spec/import/zendesk/base_factory_spec.rb | 6 + spec/import/zendesk/group_factory_spec.rb | 8 + spec/import/zendesk/group_spec.rb | 25 + .../zendesk/local_id_mapper_hook_examples.rb | 20 + .../zendesk/local_id_mapper_hook_spec.rb | 6 + .../import/zendesk/lookup_backend_examples.rb | 5 + .../zendesk/object_attribute/checkbox_spec.rb | 58 + .../zendesk/object_attribute/date_spec.rb | 59 + .../zendesk/object_attribute/decimal_spec.rb | 55 + .../zendesk/object_attribute/dropdown_spec.rb | 70 ++ .../zendesk/object_attribute/integer_spec.rb | 58 + .../zendesk/object_attribute/regexp_spec.rb | 61 + .../zendesk/object_attribute/tagger_spec.rb | 70 ++ .../zendesk/object_attribute/text_spec.rb | 55 + .../zendesk/object_attribute/textarea_spec.rb | 55 + spec/import/zendesk/object_attribute_spec.rb | 21 + spec/import/zendesk/object_field_examples.rb | 22 + spec/import/zendesk/object_field_spec.rb | 6 + .../zendesk/organization_factory_spec.rb | 8 + .../organization_field_factory_spec.rb | 8 + .../import/zendesk/organization_field_spec.rb | 6 + spec/import/zendesk/organization_spec.rb | 27 + spec/import/zendesk/priority_spec.rb | 14 + spec/import/zendesk/state_spec.rb | 14 + .../ticket/comment/attachment_factory_spec.rb | 19 + .../zendesk/ticket/comment/attachment_spec.rb | 37 + .../local_id_lookup_backend_examples.rb | 7 + .../zendesk/ticket/comment/sender_spec.rb | 6 + .../zendesk/ticket/comment/type_spec.rb | 6 + .../zendesk/ticket/comment_factory_spec.rb | 6 + spec/import/zendesk/ticket/comment_spec.rb | 637 ++++++++++ .../ticket/sub_object_factory_examples.rb | 21 + .../import/zendesk/ticket/tag_factory_spec.rb | 6 + spec/import/zendesk/ticket/tag_spec.rb | 21 + spec/import/zendesk/ticket_factory_spec.rb | 6 + .../zendesk/ticket_field_factory_spec.rb | 8 + spec/import/zendesk/ticket_field_spec.rb | 6 + spec/import/zendesk/ticket_spec.rb | 123 ++ spec/import/zendesk/user/group_spec.rb | 9 + .../zendesk/user/lookup_backend_examples.rb | 7 + spec/import/zendesk/user/role_spec.rb | 13 + spec/import/zendesk/user_factory_spec.rb | 8 + .../import/zendesk/user_field_factory_spec.rb | 8 + spec/import/zendesk/user_field_spec.rb | 6 + spec/import/zendesk/user_spec.rb | 165 +++ spec/import/zendesk_spec.rb | 6 + .../zendesk_import_browser_test.rb | 2 +- test/integration/zendesk_import_test.rb | 10 +- 104 files changed, 3267 insertions(+), 1068 deletions(-) create mode 100644 lib/import/zendesk/async.rb create mode 100644 lib/import/zendesk/base_factory.rb create mode 100644 lib/import/zendesk/group.rb create mode 100644 lib/import/zendesk/group_factory.rb create mode 100644 lib/import/zendesk/helper.rb create mode 100644 lib/import/zendesk/import_stats.rb create mode 100644 lib/import/zendesk/local_id_mapper_hook.rb create mode 100644 lib/import/zendesk/object_attribute.rb create mode 100644 lib/import/zendesk/object_attribute/checkbox.rb create mode 100644 lib/import/zendesk/object_attribute/date.rb create mode 100644 lib/import/zendesk/object_attribute/decimal.rb create mode 100644 lib/import/zendesk/object_attribute/dropdown.rb create mode 100644 lib/import/zendesk/object_attribute/integer.rb create mode 100644 lib/import/zendesk/object_attribute/regexp.rb create mode 100644 lib/import/zendesk/object_attribute/select.rb create mode 100644 lib/import/zendesk/object_attribute/tagger.rb create mode 100644 lib/import/zendesk/object_attribute/text.rb create mode 100644 lib/import/zendesk/object_attribute/textarea.rb create mode 100644 lib/import/zendesk/object_field.rb create mode 100644 lib/import/zendesk/organization.rb create mode 100644 lib/import/zendesk/organization_factory.rb create mode 100644 lib/import/zendesk/organization_field.rb create mode 100644 lib/import/zendesk/organization_field_factory.rb create mode 100644 lib/import/zendesk/priority.rb create mode 100644 lib/import/zendesk/requester.rb create mode 100644 lib/import/zendesk/state.rb create mode 100644 lib/import/zendesk/ticket.rb create mode 100644 lib/import/zendesk/ticket/comment.rb create mode 100644 lib/import/zendesk/ticket/comment/attachment.rb create mode 100644 lib/import/zendesk/ticket/comment/attachment_factory.rb create mode 100644 lib/import/zendesk/ticket/comment/sender.rb create mode 100644 lib/import/zendesk/ticket/comment/type.rb create mode 100644 lib/import/zendesk/ticket/comment_factory.rb create mode 100644 lib/import/zendesk/ticket/sub_object_factory.rb create mode 100644 lib/import/zendesk/ticket/tag.rb create mode 100644 lib/import/zendesk/ticket/tag_factory.rb create mode 100644 lib/import/zendesk/ticket_factory.rb create mode 100644 lib/import/zendesk/ticket_field.rb create mode 100644 lib/import/zendesk/ticket_field_factory.rb create mode 100644 lib/import/zendesk/user.rb create mode 100644 lib/import/zendesk/user/group.rb create mode 100644 lib/import/zendesk/user/role.rb create mode 100644 lib/import/zendesk/user_factory.rb create mode 100644 lib/import/zendesk/user_field.rb create mode 100644 lib/import/zendesk/user_field_factory.rb rename spec/import/{otrs => }/async_examples.rb (80%) rename spec/import/{otrs => }/import_stats_examples.rb (78%) create mode 100644 spec/import/zendesk/base_factory_examples.rb create mode 100644 spec/import/zendesk/base_factory_spec.rb create mode 100644 spec/import/zendesk/group_factory_spec.rb create mode 100644 spec/import/zendesk/group_spec.rb create mode 100644 spec/import/zendesk/local_id_mapper_hook_examples.rb create mode 100644 spec/import/zendesk/local_id_mapper_hook_spec.rb create mode 100644 spec/import/zendesk/lookup_backend_examples.rb create mode 100644 spec/import/zendesk/object_attribute/checkbox_spec.rb create mode 100644 spec/import/zendesk/object_attribute/date_spec.rb create mode 100644 spec/import/zendesk/object_attribute/decimal_spec.rb create mode 100644 spec/import/zendesk/object_attribute/dropdown_spec.rb create mode 100644 spec/import/zendesk/object_attribute/integer_spec.rb create mode 100644 spec/import/zendesk/object_attribute/regexp_spec.rb create mode 100644 spec/import/zendesk/object_attribute/tagger_spec.rb create mode 100644 spec/import/zendesk/object_attribute/text_spec.rb create mode 100644 spec/import/zendesk/object_attribute/textarea_spec.rb create mode 100644 spec/import/zendesk/object_attribute_spec.rb create mode 100644 spec/import/zendesk/object_field_examples.rb create mode 100644 spec/import/zendesk/object_field_spec.rb create mode 100644 spec/import/zendesk/organization_factory_spec.rb create mode 100644 spec/import/zendesk/organization_field_factory_spec.rb create mode 100644 spec/import/zendesk/organization_field_spec.rb create mode 100644 spec/import/zendesk/organization_spec.rb create mode 100644 spec/import/zendesk/priority_spec.rb create mode 100644 spec/import/zendesk/state_spec.rb create mode 100644 spec/import/zendesk/ticket/comment/attachment_factory_spec.rb create mode 100644 spec/import/zendesk/ticket/comment/attachment_spec.rb create mode 100644 spec/import/zendesk/ticket/comment/local_id_lookup_backend_examples.rb create mode 100644 spec/import/zendesk/ticket/comment/sender_spec.rb create mode 100644 spec/import/zendesk/ticket/comment/type_spec.rb create mode 100644 spec/import/zendesk/ticket/comment_factory_spec.rb create mode 100644 spec/import/zendesk/ticket/comment_spec.rb create mode 100644 spec/import/zendesk/ticket/sub_object_factory_examples.rb create mode 100644 spec/import/zendesk/ticket/tag_factory_spec.rb create mode 100644 spec/import/zendesk/ticket/tag_spec.rb create mode 100644 spec/import/zendesk/ticket_factory_spec.rb create mode 100644 spec/import/zendesk/ticket_field_factory_spec.rb create mode 100644 spec/import/zendesk/ticket_field_spec.rb create mode 100644 spec/import/zendesk/ticket_spec.rb create mode 100644 spec/import/zendesk/user/group_spec.rb create mode 100644 spec/import/zendesk/user/lookup_backend_examples.rb create mode 100644 spec/import/zendesk/user/role_spec.rb create mode 100644 spec/import/zendesk/user_factory_spec.rb create mode 100644 spec/import/zendesk/user_field_factory_spec.rb create mode 100644 spec/import/zendesk/user_field_spec.rb create mode 100644 spec/import/zendesk/user_spec.rb diff --git a/app/assets/javascripts/app/views/import/zendesk.jst.eco b/app/assets/javascripts/app/views/import/zendesk.jst.eco index 32b875196..26d8092b8 100644 --- a/app/assets/javascripts/app/views/import/zendesk.jst.eco +++ b/app/assets/javascripts/app/views/import/zendesk.jst.eco @@ -31,7 +31,9 @@

<%- @T('Enter your Email address and the Zendesk API token gained from your') %> <%- @T('admin interface') %> - +

+

+ <%- @T('Attention: These will be your login credentials after the import is completed.') %>

diff --git a/lib/import/base_factory.rb b/lib/import/base_factory.rb index 550d5ebd6..0fe6746e6 100644 --- a/lib/import/base_factory.rb +++ b/lib/import/base_factory.rb @@ -4,13 +4,25 @@ module Import # rubocop:disable Style/ModuleFunction extend self + def import_action(records, *args) + pre_import_hook(records) + import_loop(records) do |record| + next if skip?(record) + backend_instance = create_instance(record, *args) + post_import_hook(record, backend_instance) + end + end + def import(_records) - raise 'Missing implementation for import method for this factory' + raise 'Missing import method implementation for this factory' end def pre_import_hook(_records) end + def post_import_hook(_record, _backend_instance) + end + def backend_class(_record) "Import::#{module_name}".constantize end @@ -21,6 +33,14 @@ module Import private + def create_instance(record, *args) + backend_class(record).new(record, *args) + end + + def import_loop(records, &import_block) + records.each(&import_block) + end + def module_name name.to_s.sub(/Import::/, '').sub(/Factory/, '') end diff --git a/lib/import/factory.rb b/lib/import/factory.rb index baf6c35ed..1a5dc206b 100644 --- a/lib/import/factory.rb +++ b/lib/import/factory.rb @@ -4,13 +4,6 @@ module Import # rubocop:disable Style/ModuleFunction extend self - - def import(records) - pre_import_hook(records) - records.each do |record| - next if skip?(record) - backend_class(record).new(record) - end - end + alias import import_action end end diff --git a/lib/import/transaction_factory.rb b/lib/import/transaction_factory.rb index e35771a9a..8f1af13fb 100644 --- a/lib/import/transaction_factory.rb +++ b/lib/import/transaction_factory.rb @@ -7,11 +7,7 @@ module Import def import(records) ActiveRecord::Base.transaction do - pre_import_hook(records) - records.each do |record| - next if skip?(record) - backend_class(record).new(record) - end + import_action(records) end end end diff --git a/lib/import/zendesk.rb b/lib/import/zendesk.rb index 815df8146..598c3ceff 100644 --- a/lib/import/zendesk.rb +++ b/lib/import/zendesk.rb @@ -4,1067 +4,48 @@ require 'zendesk_api' module Import end module Import::Zendesk + extend Import::Helper + extend Import::Zendesk::Async + extend Import::Zendesk::ImportStats # rubocop:disable Style/ModuleFunction extend self def start - Rails.logger.info 'Start import...' + log 'Start import...' - # check if system is in import mode - if !Setting.get('import_mode') - raise 'System is not in import mode!' - end + checks - initialize_client + Import::Zendesk::GroupFactory.import(client.groups) - import_fields + Import::Zendesk::OrganizationFieldFactory.import(client.organization_fields) + Import::Zendesk::OrganizationFactory.import(client.organizations) + + Import::Zendesk::UserFieldFactory.import(client.user_fields) + Import::Zendesk::UserFactory.import(client.users) + + Import::Zendesk::TicketFieldFactory.import(client.ticket_fields) + Import::Zendesk::TicketFactory.import(client.tickets) # TODO - # import_oauth - # import_twitter_channel - - import_groups - - import_organizations - - import_users - - import_tickets - - # TODO - # import_sla_policies - - # import_macros - - # import_schedules - - # import_views - - # import_automations - Setting.set( 'system_init_done', true ) Setting.set( 'import_mode', false ) true end -=begin - start import in background - - Import::Zendesk.start_bg -=end - - def start_bg - Setting.reload - - Import::Zendesk.connection_test - - # get statistic before starting import - statistic - - # start thread to observe current state - status_update_thread = Thread.new { - loop do - result = { - data: current_state, - result: 'in_progress', - } - Cache.write('import:state', result, expires_in: 10.minutes) - sleep 8 - end - } - sleep 2 - - # start import data - begin - Import::Zendesk.start - rescue => e - status_update_thread.exit - status_update_thread.join - Rails.logger.error e.message - Rails.logger.error e.backtrace.inspect - result = { - message: e.message, - result: 'error', - } - Cache.write('import:state', result, expires_in: 10.hours) - return false - end - sleep 16 # wait until new finished import state is on client - status_update_thread.exit - status_update_thread.join - - result = { - result: 'import_done', - } - Cache.write('import:state', result, expires_in: 10.hours) - - Setting.set('system_init_done', true) - Setting.set('import_mode', false) - end - -=begin - - get import state from background process - - result = Import::Zendesk.status_bg - -=end - - def status_bg - state = Cache.get('import:state') - return state if state - { - message: 'not running', - } - end - -=begin - - start get request to backend to check connection - - result = connection_test - - return - - true | false - -=end - def connection_test - initialize_client - - return true if @client.users.first - false + Import::Zendesk::Requester.connection_test end - def statistic + private - # check cache - cache = Cache.get('import_zendesk_stats') - if cache - return cache - end - - initialize_client - - # retrive statistic - result = { - 'Tickets' => 0, - 'TicketFields' => 0, - 'UserFields' => 0, - 'OrganizationFields' => 0, - 'Groups' => 0, - 'Organizations' => 0, - 'Users' => 0, - 'GroupMemberships' => 0, - 'Macros' => 0, - 'Views' => 0, - 'Automations' => 0, - } - - result.each { |object, _score| - result[ object ] = @client.send( object.underscore.to_sym ).count! - } - - if result - Cache.write('import_zendesk_stats', result) - end - result + def client + Import::Zendesk::Requester.client end -=begin - - return current import state - - result = current_state - - return - - { - Group: { - total: 1234, - done: 13, - }, - Organization: { - total: 1234, - done: 13, - }, - User: { - total: 1234, - done: 13, - }, - Ticket: { - total: 1234, - done: 13, - }, - } - -=end - - def current_state - - data = statistic - - { - Group: { - done: Group.count, - total: data['Groups'] || 0, - }, - Organization: { - done: Organization.count, - total: data['Organizations'] || 0, - }, - User: { - done: User.count, - total: data['Users'] || 0, - }, - Ticket: { - done: Ticket.count, - total: data['Tickets'] || 0, - }, - } - end - - def initialize_client - @client = ZendeskAPI::Client.new do |config| - config.url = Setting.get('import_zendesk_endpoint') - - # Basic / Token Authentication - config.username = Setting.get('import_zendesk_endpoint_username') - config.token = Setting.get('import_zendesk_endpoint_key') - - # when hitting the rate limit, sleep automatically, - # then retry the request. - config.retry = true - end - end - - def mapping_state(zendesk_state) - - mapping = { - 'pending' => 'pending reminder', - 'solved' => 'closed', - } - return zendesk_state if !mapping[zendesk_state] - mapping[zendesk_state] - end - - def mapping_priority(zendesk_priority) - - mapping = { - 'low' => '1 low', - nil => '2 normal', - 'normal' => '2 normal', - 'high' => '3 high', - 'urgent' => '3 high', - } - mapping[zendesk_priority] - end - - # NOT IMPLEMENTED YET - def mapping_type(zendesk_type) - - mapping = { - nil => '', - 'question' => '', - 'incident' => '', - 'problem' => '', - 'task' => '', - } - return zendesk_type if !mapping[zendesk_type] - mapping[zendesk_type] - end - - def mapping_ticket_field(zendesk_field) - - mapping = { - 'subject' => 'title', - 'description' => 'note', - 'status' => 'state_id', - 'tickettype' => 'type', - 'priority' => 'priority_id', - 'group' => 'group_id', - 'assignee' => 'owner_id', - } - return zendesk_field if !mapping[zendesk_field] - mapping[zendesk_field] - end - - # FILTER: - # TODO: - # https://developer.zendesk.com/rest_api/docs/core/views#conditions-reference - def mapping_filter(zendesk_filter) - - end - - # Ticket Fields - # User Fields - # Organization Fields - # TODO: - # https://developer.zendesk.com/rest_api/docs/core/ticket_fields - # https://developer.zendesk.com/rest_api/docs/core/user_fields - # https://developer.zendesk.com/rest_api/docs/core/organization_fields - def import_fields - - %w(Ticket User Organization).each { |local_object| - - local_fields = local_object.constantize.column_names - - @client.send("#{local_object.downcase}_fields").all! { |zendesk_object_field| - - if local_object == 'Ticket' - mapped_object_field = method("mapping_#{local_object.downcase}_field").call( zendesk_object_field.type ) - - next if local_fields.include?( mapped_object_field ) - end - - import_field(local_object, zendesk_object_field) - } - } - end - - def import_field(local_object, zendesk_field) - - name = '' - name = if local_object == 'Ticket' - zendesk_field.title - else - zendesk_field['key'] # TODO: y?! - end - - @zendesk_field_mapping ||= {} - @zendesk_field_mapping[ zendesk_field.id ] = name - - data_type = zendesk_field.type - data_option = { - null: !zendesk_field.required, - note: zendesk_field.description, - } - - if zendesk_field.type == 'date' - data_option = { - future: true, - past: true, - diff: 0, - }.merge(data_option) - elsif zendesk_field.type == 'checkbox' - data_type = 'boolean' - data_option = { - default: false, - options: { - true => 'yes', - false => 'no', - }, - }.merge(data_option) - elsif zendesk_field.type == 'regexp' - data_type = 'input' - data_option = { - type: 'text', - maxlength: 255, - regex: zendesk_field.regexp_for_validation, - }.merge(data_option) - elsif zendesk_field.type == 'decimal' - data_type = 'input' - data_option = { - type: 'text', - maxlength: 255, - }.merge(data_option) - elsif zendesk_field.type == 'integer' - data_type = 'integer' - data_option = { - min: 0, - max: 999_999_999, - }.merge(data_option) - elsif zendesk_field.type == 'text' - data_type = 'input' - data_option = { - type: zendesk_field.type, - maxlength: 255, - }.merge(data_option) - elsif zendesk_field.type == 'textarea' - data_type = 'input' - data_option = { - type: zendesk_field.type, - maxlength: 255, - }.merge(data_option) - elsif zendesk_field.type == 'tagger' || zendesk_field.type == 'dropdown' - - # \"custom_field_options\"=>[{\"id\"=>28353445 - # \"name\"=>\"Another Value\" - # \"raw_name\"=>\"Another Value\" - # \"value\"=>\"anotherkey\"} - # {\"id\"=>28353425 - # \"name\"=>\"Value 1\" - # \"raw_name\"=>\"Value 1\" - # \"value\"=>\"key1\"} - # {\"id\"=>28353435 - # \"name\"=>\"Value 2\" - # \"raw_name\"=>\"Value 2\" - # \"value\"=>\"key2\"}]}> - # " - - options = {} - @zendesk_ticket_field_value_mapping ||= {} - zendesk_field.custom_field_options.each { |entry| - - if local_object == 'Ticket' - @zendesk_ticket_field_value_mapping[ name ] ||= {} - @zendesk_ticket_field_value_mapping[ name ][ entry['id'] ] = entry['value'] - end - - options[ entry['value'] ] = entry['name'] - } - - data_type = 'select' - data_option = { - default: '', - options: options, - }.merge(data_option) - end - - screens = { - view: { - '-all-' => { - shown: true, - }, - } - } - - if zendesk_field.visible_in_portal || !zendesk_field.required_in_portal - screens = { - edit: { - Customer: { - shown: zendesk_field.visible_in_portal, - null: !zendesk_field.required_in_portal, - }, - }.merge(screens) - } - end - name.gsub!(/\s/, '_') - - ObjectManager::Attribute.add( - object: local_object, - name: name, - display: zendesk_field.title, - data_type: data_type, - data_option: data_option, - editable: !zendesk_field.removable, - active: zendesk_field.active, - screens: screens, - position: zendesk_field.position, - created_by_id: 1, - updated_by_id: 1, - ) - ObjectManager::Attribute.migration_execute(false) - end - - # OAuth - # TODO: - # https://developer.zendesk.com/rest_api/docs/core/oauth_tokens - # https://developer.zendesk.com/rest_api/docs/core/oauth_clients - def import_oauth - - end - - # Twitter - # TODO: - # https://developer.zendesk.com/rest_api/docs/core/twitter_channel - def import_twitter - - end - - # Groups - # https://developer.zendesk.com/rest_api/docs/core/groups - def import_groups - - @zendesk_group_mapping = {} - @client.groups.all! { |zendesk_group| - local_group = Group.create_if_not_exists( - name: zendesk_group.name, - active: !zendesk_group.deleted, - updated_by_id: 1, - created_by_id: 1 - ) - - @zendesk_group_mapping[ zendesk_group.id ] = local_group.id - } - end - - # Organizations - # https://developer.zendesk.com/rest_api/docs/core/organizations - def import_organizations - - @zendesk_organization_mapping = {} - - @client.organizations.each { |zendesk_organization| - custom_fields = get_fields(zendesk_organization.organization_fields) - - local_organization_fields = { - name: zendesk_organization.name, - note: zendesk_organization.note, - shared: zendesk_organization.shared_tickets, - # shared: zendesk_organization.shared_comments, # TODO, not yet implemented - # }.merge(zendesk_organization.organization_fields) # TODO - updated_by_id: 1, - created_by_id: 1 - }.merge(custom_fields) - - local_organization = Organization.create_if_not_exists(local_organization_fields) - @zendesk_organization_mapping[ zendesk_organization.id ] = local_organization.id - } - end - - # Users - # https://developer.zendesk.com/rest_api/docs/core/users - def import_users - import_group_memberships - import_custom_roles - - @zendesk_user_mapping = {} - - role_admin = Role.lookup(name: 'Admin') - role_agent = Role.lookup(name: 'Agent') - role_customer = Role.lookup(name: 'Customer') - - @client.users.all! { |zendesk_user| - custom_fields = get_fields(zendesk_user.user_fields) - local_user_fields = { - login: zendesk_user.id.to_s, # Zendesk users may have no other identifier than the ID, e.g. twitter users - firstname: zendesk_user.name, - email: zendesk_user.email, - phone: zendesk_user.phone, - password: '', - active: !zendesk_user.suspended, - groups: [], - roles: [], - note: zendesk_user.notes, - verified: zendesk_user.verified, - organization_id: @zendesk_organization_mapping[ zendesk_user.organization_id ], - last_login: zendesk_user.last_login_at, - updated_by_id: 1, - created_by_id: 1 - }.merge(custom_fields) - - if @zendesk_user_group_mapping[ zendesk_user.id ] - - @zendesk_user_group_mapping[ zendesk_user.id ].each { |zendesk_group_id| - - local_group_id = @zendesk_group_mapping[ zendesk_group_id ] - - next if !local_group_id - - group = Group.find( local_group_id ) - - local_user_fields[:groups].push group - } - end - - if zendesk_user.role.name == 'end-user' - local_user_fields[:roles].push role_customer - - elsif zendesk_user.role.name == 'agent' - - local_user_fields[:roles].push role_agent - - if !zendesk_user.restricted_agent - local_user_fields[:roles].push role_admin - end - - elsif zendesk_user.role.name == 'admin' - local_user_fields[:roles].push role_agent - local_user_fields[:roles].push role_admin - end - - if zendesk_user.photo && zendesk_user.photo.content_url - local_user_fields[:image_source] = zendesk_user.photo.content_url - end - - # TODO - # local_user_fields = local_user_fields.merge( user.user_fields ) - - # TODO - # user.custom_role_id (Enterprise only) - local_user = User.create_or_update( local_user_fields ) - - @zendesk_user_mapping[ zendesk_user.id ] = local_user.id - } - end - - # Group Memberships - # TODO: - # https://developer.zendesk.com/rest_api/docs/core/group_memberships - def import_group_memberships - - @zendesk_user_group_mapping = {} - - @client.group_memberships.all! { |zendesk_group_membership| - - @zendesk_user_group_mapping[ zendesk_group_membership.user_id ] ||= [] - @zendesk_user_group_mapping[ zendesk_group_membership.user_id ].push( zendesk_group_membership.group_id ) - } - end - - # Custom Roles (Enterprise only) - # TODO: - # https://developer.zendesk.com/rest_api/docs/core/custom_roles - def import_custom_roles - - end - - # Tickets - # https://developer.zendesk.com/rest_api/docs/core/tickets - # https://developer.zendesk.com/rest_api/docs/core/ticket_comments#ticket-comments - # https://developer.zendesk.com/rest_api/docs/core/ticket_audits#the-via-object - # https://developer.zendesk.com/rest_api/docs/help_center/article_attachments - # https://developer.zendesk.com/rest_api/docs/core/ticket_audits # v2 - def import_tickets - - article_sender_customer = Ticket::Article::Sender.lookup(name: 'Customer') - article_sender_agent = Ticket::Article::Sender.lookup(name: 'Agent') - article_sender_system = Ticket::Article::Sender.lookup(name: 'System') - - initialize_article_types - - @client.tickets.all! { |zendesk_ticket| - custom_fields = get_custom_fields(zendesk_ticket.custom_fields) - local_ticket_fields = { - id: zendesk_ticket.id, - title: zendesk_ticket.subject, - note: zendesk_ticket.description, - group_id: @zendesk_group_mapping[ zendesk_ticket.group_id ] || 1, - customer_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ] || 1, - organization_id: @zendesk_organization_mapping[ zendesk_ticket.organization_id ], - state: Ticket::State.lookup( name: mapping_state( zendesk_ticket.status ) ), - priority: Ticket::Priority.lookup( name: mapping_priority( zendesk_ticket.priority ) ), - pending_time: zendesk_ticket.due_at, - updated_at: zendesk_ticket.updated_at, - created_at: zendesk_ticket.created_at, - updated_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ] || 1, - created_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ] || 1, - }.merge(custom_fields) - ticket_author = User.find( @zendesk_user_mapping[ zendesk_ticket.requester_id ] || 1 ) - - local_ticket_fields[:create_article_sender_id] = if ticket_author.role?('Customer') - article_sender_customer.id - elsif ticket_author.role?('Agent') - article_sender_agent.id - else - article_sender_system.id - end - - local_ticket_fields[:create_article_type_id] = article_type_lookup(zendesk_ticket.via) - - local_ticket = Ticket.find_by(id: local_ticket_fields[:id]) - if local_ticket - local_ticket.update_attributes(local_ticket_fields) - else - local_ticket = Ticket.create(local_ticket_fields) - _reset_pk('tickets') - end - - zendesk_ticket_tags = [] - zendesk_ticket.tags.each { |tag| - zendesk_ticket_tags.push(tag) - } - - zendesk_ticket_tags.each { |tag| - Tag.tag_add( - object: 'Ticket', - o_id: local_ticket.id, - item: tag.id, - created_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ] || 1, - ) - } - - zendesk_ticket_articles = [] - zendesk_ticket.comments.each { |zendesk_article| - zendesk_ticket_articles.push(zendesk_article) - } - - zendesk_ticket_articles.each { |zendesk_article| - - local_article_fields = { - ticket_id: local_ticket.id, - body: zendesk_article.html_body, - internal: !zendesk_article.public, - message_id: zendesk_article.id, - updated_by_id: @zendesk_user_mapping[ zendesk_article.author_id ] || 1, - created_by_id: @zendesk_user_mapping[ zendesk_article.author_id ] || 1, - } - - article_author = User.find( @zendesk_user_mapping[ zendesk_article.author_id ] || 1 ) - - local_article_fields[:sender_id] = if article_author.role?('Customer') - article_sender_customer.id - elsif article_author.role?('Agent') - article_sender_agent.id - else - article_sender_system.id - end - - local_article_fields[:type_id] = article_type_lookup(zendesk_article.via) - - if zendesk_article.via.channel == 'email' - local_article_fields[:from] = zendesk_article.via.source.from.address - local_article_fields[:to] = zendesk_article.via.source.to.address # Notice zendesk_article.via.from.original_recipients=[\"another@gmail.com\", \"support@example.zendesk.com\"] - elsif zendesk_article.via.channel == 'facebook' - local_article_fields[:from] = zendesk_article.via.source.from.facebook_id - local_article_fields[:to] = zendesk_article.via.source.to.facebook_id - end - - # create article - local_article = Ticket::Article.find_by(message_id: local_article_fields[:message_id]) - if local_article - local_article.update_attributes(local_article_fields) - else - local_article = Ticket::Article.create( local_article_fields ) - end - - zendesk_attachments = zendesk_article.attachments - - next if zendesk_attachments.size.zero? - - local_attachments = local_article.attachments - - zendesk_ticket_attachments = [] - zendesk_attachments.each { |zendesk_attachment| - zendesk_ticket_attachments.push(zendesk_attachment) - } - - zendesk_ticket_attachments.each { |zendesk_attachment| - - response = UserAgent.get( - zendesk_attachment.content_url, - {}, - { - open_timeout: 10, - read_timeout: 60, - }, - ) - - if !response.success? - Rails.logger.error response.error - next - end - - local_attachment = Store.add( - object: 'Ticket::Article', - o_id: local_article.id, - data: response.body, - filename: zendesk_attachment.file_name, - preferences: { - 'Content-Type' => zendesk_attachment.content_type - }, - created_by_id: 1 - ) - } - } - } - end - - # SLA Policies - # TODO: - # https://github.com/zendesk/zendesk_api_client_rb/issues/271 - # https://developer.zendesk.com/rest_api/docs/core/sla_policies - def import_sla_policies - - end - - # Macros - # TODO: - # https://developer.zendesk.com/rest_api/docs/core/macros - def import_macros - - @client.macros.all! { |zendesk_macro| - - # TODO - next if !zendesk_macro.active - - # "url"=>"https://example.zendesk.com/api/v2/macros/59511191.json" - # "id"=>59511191 - # "title"=>"Herabstufen und informieren" - # "active"=>true - # "updated_at"=>2015-08-03 13:51:14 UTC - # "created_at"=>2015-07-19 22:41:42 UTC - # "restriction"=>nil - # "actions"=>[ - # { - # "field"=>"priority" - # "value"=>"low" - # } - # { - # "field"=>"comment_value" - # "value"=>"Das Verkehrsaufkommen ist g....." - # } - # ] - - perform = {} - zendesk_macro.actions.each { |action| - - # TODO: ID fields - perform["ticket.#{action.field}"] = action.value - } - - Macro.create_if_not_exists( - name: zendesk_macro.title, - perform: perform, - note: '', - active: zendesk_macro.active, - ) - } - end - - # Schedulers - # TODO: - # https://github.com/zendesk/zendesk_api_client_rb/issues/281 - # https://developer.zendesk.com/rest_api/docs/core/schedules - def import_schedules - - end - - # Views - # TODO: - # https://developer.zendesk.com/rest_api/docs/core/views - def import_views - - @client.views.all! { |zendesk_view| - - # "url" => "https://example.zendesk.com/api/v2/views/59511071.json" - # "id" => 59511071 - # "title" => "Ihre Tickets" - # "active" => true - # "updated_at" => 2015-08-03 13:51:14 UTC - # "created_at" => 2015-07-19 22:41:42 UTC - # "restriction" => nil - # "sla_id" => nil - # "execution" => { - # "group_by" => "status" - # "group_order" => "asc" - # "sort_by" => "score" - # "sort_order" => "desc" - # "group" => { - # "id" => "status" - # "title" => "Status" - # "order" => "asc" - # } - # "sort" => { - # "id" => "score" - # "title" => "Score" - # "order" => "desc" - # } - # "columns" => [ - # { - # "id" => "score" - # "title" => "Score" - # } - # { - # "id" => "subject" - # "title" => "Subject" - # } - # { - # "id" => "requester" - # "title" => "Requester" - # } - # { - # "id" => "created" - # "title" => "Requested" - # } - # { - # "id" => "type" - # "title" => "Type" - # } - # { - # "id" => "priority" - # "title" => "Priority" - # } - # ] - # "fields" => [ - # { - # "id" => "score" - # "title" => "Score" - # } - # { - # "id" => "subject" - # "title" => "Subject" - # } - # { - # "id" => "requester" - # "title" => "Requester" - # } - # { - # "id" => "created" - # "title" => "Requested" - # } - # { - # "id" => "type" - # "title" => "Type" - # } - # { - # "id" => "priority" - # "title" => "Priority" - # } - # ] - # "custom_fields" => [] - # } - # "conditions" => { - # "all" => [ - # { - # "field" => "status" - # "operator" => "less_than" - # "value" => "solved" - # } - # { - # "field" => "assignee_id" - # "operator" => "is" - # "value" => "current_user" - # } - # ] - # "any" => [] - # } - - Overview.create_if_not_exists( - name: zendesk_view.title, - link: 'my_assigned', # TODO - prio: 1000, - role_id: overview_role.id, - condition: { - 'ticket.state_id' => { - operator: 'is', - value: [ 1, 2, 3, 7 ], - }, - 'ticket.owner_id' => { - operator: 'is', - pre_condition: 'current_user.id', - }, - }, - order: { - by: 'created_at', - direction: 'ASC', - }, - view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), - view_mode_default: 's', - }, - ) - } - end - - # Automations - # TODO: - # https://developer.zendesk.com/rest_api/docs/core/automations - def import_automations - - @client.automations.all! { |_zendesk_automation| - - # "url" => "https://example.zendesk.com/api/v2/automations/60037892.json" - # "id" => 60037892 - # "title" => "Ticket aus Facebook-Nachricht 1 ..." - # "active" => true - # "updated_at" => 2015-08-03 13:51:15 UTC - # "created_at" => 2015-07-28 11:27:50 UTC - # "actions" => [ - # { - # "field" => "status" - # "value" => "closed" - # } - # ] - # "conditions" => { - # "all" => [ - # { - # "field" => "status" - # "operator" => "is" - # "value" => "solved" - # } - # { - # "field" => "SOLVED" - # "operator" => "is" - # "value" => "24" - # } - # { - # "field" => "via_type" - # "operator" => "is" - # "value" => "facebook" - # } - # ] - # "any" => [] - # } - # "position" => 10000 - - } - end - - # reset primary key sequences - def self._reset_pk(table) - DbHelper.import_post(table) - end - - def get_custom_fields(custom_fields) - return {} if !custom_fields - fields = {} - custom_fields.each { |custom_field| - field_name = @zendesk_field_mapping[ custom_field['id'] ].gsub(/\s/, '_') - field_value = custom_field['value'] - next if field_value.nil? # ignore nil values - if @zendesk_ticket_field_value_mapping[ field_name ] - field_value = @zendesk_ticket_field_value_mapping[ field_name ][ field_value ] - end - fields[ field_name.to_sym ] = field_value - } - fields - end - - def get_fields(user_fields) - return {} if !user_fields - fields = {} - user_fields.each { |key, value| - fields[key] = value - } - fields - end - - def initialize_article_types - article_types = ['web', 'note', 'email', 'twitter status', - 'twitter direct-message', 'facebook feed post', - 'facebook feed comment'] - @article_type_id = {} - article_types.each do |article_type| - - article_type_key = article_type.gsub(/\s|\-/, '_').to_sym - - @article_type_id[article_type_key] = Ticket::Article::Type.lookup(name: article_type).id - end - end - - def article_type_lookup(via) - case via.channel - when 'web' - @article_type_id[:web] - when 'email' - @article_type_id[:email] - when 'sample_ticket' - @article_type_id[:note] - when 'twitter' - if via.source.rel == 'mention' - @article_type_id[:twitter_status] - else - @article_type_id[:twitter_direct_message] - end - when 'facebook' - if via.source.rel == 'post' - @article_type_id[:facebook_feed_post] - else - @article_type_id[:facebook_feed_comment] - end - # fallback for other not (yet) supported article types - # See: - # https://support.zendesk.com/hc/en-us/articles/203661746-Zendesk-Glossary#topic_zie_aqe_tf - # https://support.zendesk.com/hc/en-us/articles/203661596-About-Zendesk-Support-channels - else - @article_type_id[:web] - end + def checks + check_import_mode + connection_test end end diff --git a/lib/import/zendesk/async.rb b/lib/import/zendesk/async.rb new file mode 100644 index 000000000..31df71b68 --- /dev/null +++ b/lib/import/zendesk/async.rb @@ -0,0 +1,65 @@ +module Import + module Zendesk + module Async + # rubocop:disable Style/ModuleFunction + extend self + + def start_bg + Setting.reload + + Import::Zendesk.connection_test + + # get statistic before starting import + statistic + + # start thread to observe current state + status_update_thread = Thread.new { + loop do + result = { + data: current_state, + result: 'in_progress', + } + Cache.write('import:state', result, expires_in: 10.minutes) + sleep 8 + end + } + sleep 2 + + # start import data + begin + Import::Zendesk.start + rescue => e + status_update_thread.exit + status_update_thread.join + Rails.logger.error e.message + Rails.logger.error e.backtrace.inspect + result = { + message: e.message, + result: 'error', + } + Cache.write('import:state', result, expires_in: 10.hours) + return false + end + sleep 16 # wait until new finished import state is on client + status_update_thread.exit + status_update_thread.join + + result = { + result: 'import_done', + } + Cache.write('import:state', result, expires_in: 10.hours) + + Setting.set('system_init_done', true) + Setting.set('import_mode', false) + end + + def status_bg + state = Cache.get('import:state') + return state if state + { + message: 'not running', + } + end + end + end +end diff --git a/lib/import/zendesk/base_factory.rb b/lib/import/zendesk/base_factory.rb new file mode 100644 index 000000000..5ec8fdfdb --- /dev/null +++ b/lib/import/zendesk/base_factory.rb @@ -0,0 +1,16 @@ +module Import + module Zendesk + module BaseFactory + include Import::Factory + + # rubocop:disable Style/ModuleFunction + extend self + + private + + def import_loop(records, &import_block) + records.all!(&import_block) + end + end + end +end diff --git a/lib/import/zendesk/group.rb b/lib/import/zendesk/group.rb new file mode 100644 index 000000000..0c0c14566 --- /dev/null +++ b/lib/import/zendesk/group.rb @@ -0,0 +1,21 @@ +module Import + module Zendesk + class Group + include Import::Helper + + attr_reader :zendesk_id, :id + + def initialize(group) + local_group = ::Group.create_if_not_exists( + name: group.name, + active: !group.deleted, + updated_by_id: 1, + created_by_id: 1 + ) + + @zendesk_id = group.id + @id = local_group.id + end + end + end +end diff --git a/lib/import/zendesk/group_factory.rb b/lib/import/zendesk/group_factory.rb new file mode 100644 index 000000000..3386da928 --- /dev/null +++ b/lib/import/zendesk/group_factory.rb @@ -0,0 +1,8 @@ +module Import + module Zendesk + module GroupFactory + extend Import::Zendesk::BaseFactory + extend Import::Zendesk::LocalIDMapperHook + end + end +end diff --git a/lib/import/zendesk/helper.rb b/lib/import/zendesk/helper.rb new file mode 100644 index 000000000..bdd210f57 --- /dev/null +++ b/lib/import/zendesk/helper.rb @@ -0,0 +1,19 @@ +module Import + module Zendesk + module Helper + # rubocop:disable Style/ModuleFunction + extend self + + private + + def get_fields(zendesk_fields) + return {} if !zendesk_fields + fields = {} + zendesk_fields.each { |key, value| + fields[key] = value + } + fields + end + end + end +end diff --git a/lib/import/zendesk/import_stats.rb b/lib/import/zendesk/import_stats.rb new file mode 100644 index 000000000..c49a8f0df --- /dev/null +++ b/lib/import/zendesk/import_stats.rb @@ -0,0 +1,61 @@ +module Import + module Zendesk + module ImportStats + # rubocop:disable Style/ModuleFunction + extend self + + def current_state + + data = statistic + + { + Group: { + done: ::Group.count, + total: data['Groups'] || 0, + }, + Organization: { + done: ::Organization.count, + total: data['Organizations'] || 0, + }, + User: { + done: ::User.count, + total: data['Users'] || 0, + }, + Ticket: { + done: ::Ticket.count, + total: data['Tickets'] || 0, + }, + } + end + + def statistic + + # check cache + cache = Cache.get('import_zendesk_stats') + return cache if cache + + # retrive statistic + result = { + 'Tickets' => 0, + 'TicketFields' => 0, + 'UserFields' => 0, + 'OrganizationFields' => 0, + 'Groups' => 0, + 'Organizations' => 0, + 'Users' => 0, + 'GroupMemberships' => 0, + 'Macros' => 0, + 'Views' => 0, + 'Automations' => 0, + } + + result.each { |object, _score| + result[ object ] = Import::Zendesk::Requester.client.send( object.underscore.to_sym ).count! + } + + Cache.write('import_zendesk_stats', result) + result + end + end + end +end diff --git a/lib/import/zendesk/local_id_mapper_hook.rb b/lib/import/zendesk/local_id_mapper_hook.rb new file mode 100644 index 000000000..b300caa9a --- /dev/null +++ b/lib/import/zendesk/local_id_mapper_hook.rb @@ -0,0 +1,25 @@ +module Import + module Zendesk + module LocalIDMapperHook + + # rubocop:disable Style/ModuleFunction + extend self + + def local_id(zendesk_id) + init_mapping + @zendesk_mapping[ zendesk_id ] + end + + def post_import_hook(_record, backend_instance) + init_mapping + @zendesk_mapping[ backend_instance.zendesk_id ] = backend_instance.id + end + + private + + def init_mapping + @zendesk_mapping ||= {} + end + end + end +end diff --git a/lib/import/zendesk/object_attribute.rb b/lib/import/zendesk/object_attribute.rb new file mode 100644 index 000000000..4c8824487 --- /dev/null +++ b/lib/import/zendesk/object_attribute.rb @@ -0,0 +1,73 @@ +module Import + module Zendesk + class ObjectAttribute + + def initialize(object, name, attribute) + + initialize_data_option(attribute) + init_callback(attribute) + + add(object, name, attribute) + end + + private + + def init_callback(_attribute) + raise 'Missing init_callback method implementation for this object attribute' + end + + def add(object, name, attribute) + ObjectManager::Attribute.add( attribute_config(object, name, attribute) ) + ObjectManager::Attribute.migration_execute(false) + end + + def attribute_config(object, name, attribute) + { + object: object, + name: name, + display: attribute.title, + data_type: data_type(attribute), + data_option: @data_option, + editable: !attribute.removable, + active: attribute.active, + screens: screens(attribute), + position: attribute.position, + created_by_id: 1, + updated_by_id: 1, + } + end + + def screens(attribute) + config = { + view: { + '-all-' => { + shown: true, + }, + } + } + + return config if !attribute.visible_in_portal && attribute.required_in_portal + + { + edit: { + Customer: { + shown: attribute.visible_in_portal, + null: !attribute.required_in_portal, + }, + }.merge(config) + } + end + + def initialize_data_option(attribute) + @data_option = { + null: !attribute.required, + note: attribute.description, + } + end + + def data_type(attribute) + attribute.type + end + end + end +end diff --git a/lib/import/zendesk/object_attribute/checkbox.rb b/lib/import/zendesk/object_attribute/checkbox.rb new file mode 100644 index 000000000..143b629e6 --- /dev/null +++ b/lib/import/zendesk/object_attribute/checkbox.rb @@ -0,0 +1,23 @@ +module Import + module Zendesk + class ObjectAttribute + class Checkbox < Import::Zendesk::ObjectAttribute + def init_callback(_object_attribte) + @data_option.merge!( + default: false, + options: { + true => 'yes', + false => 'no', + }, + ) + end + + private + + def data_type(_attribute) + 'boolean' + end + end + end + end +end diff --git a/lib/import/zendesk/object_attribute/date.rb b/lib/import/zendesk/object_attribute/date.rb new file mode 100644 index 000000000..ed7ba4165 --- /dev/null +++ b/lib/import/zendesk/object_attribute/date.rb @@ -0,0 +1,20 @@ +# this require is required (hehe) because of Rails autoloading +# which causes strange behavior not inheriting correctly +# from Import::OTRS::DynamicField +require 'import/zendesk/object_attribute' + +module Import + module Zendesk + class ObjectAttribute + class Date < Import::Zendesk::ObjectAttribute + def init_callback(_object_attribte) + @data_option.merge!( + future: true, + past: true, + diff: 0, + ) + end + end + end + end +end diff --git a/lib/import/zendesk/object_attribute/decimal.rb b/lib/import/zendesk/object_attribute/decimal.rb new file mode 100644 index 000000000..6067a7540 --- /dev/null +++ b/lib/import/zendesk/object_attribute/decimal.rb @@ -0,0 +1,8 @@ +module Import + module Zendesk + class ObjectAttribute + class Decimal < Import::Zendesk::ObjectAttribute::Text + end + end + end +end diff --git a/lib/import/zendesk/object_attribute/dropdown.rb b/lib/import/zendesk/object_attribute/dropdown.rb new file mode 100644 index 000000000..871e87df1 --- /dev/null +++ b/lib/import/zendesk/object_attribute/dropdown.rb @@ -0,0 +1,8 @@ +module Import + module Zendesk + class ObjectAttribute + class Dropdown < Import::Zendesk::ObjectAttribute::Select + end + end + end +end diff --git a/lib/import/zendesk/object_attribute/integer.rb b/lib/import/zendesk/object_attribute/integer.rb new file mode 100644 index 000000000..d16e94233 --- /dev/null +++ b/lib/import/zendesk/object_attribute/integer.rb @@ -0,0 +1,25 @@ +# this require is required (hehe) because of Rails autoloading +# which causes strange behavior not inheriting correctly +# from Import::OTRS::DynamicField +require 'import/zendesk/object_attribute' + +module Import + module Zendesk + class ObjectAttribute + class Integer < Import::Zendesk::ObjectAttribute + def init_callback(_object_attribte) + @data_option.merge!( + min: 0, + max: 999_999_999, + ) + end + + private + + def data_type(_attribute) + 'integer' + end + end + end + end +end diff --git a/lib/import/zendesk/object_attribute/regexp.rb b/lib/import/zendesk/object_attribute/regexp.rb new file mode 100644 index 000000000..b557dab66 --- /dev/null +++ b/lib/import/zendesk/object_attribute/regexp.rb @@ -0,0 +1,26 @@ +# this require is required (hehe) because of Rails autoloading +# which causes strange behavior not inheriting correctly +# from Import::OTRS::DynamicField +require 'import/zendesk/object_attribute' + +module Import + module Zendesk + class ObjectAttribute + class Regexp < Import::Zendesk::ObjectAttribute + def init_callback(object_attribte) + @data_option.merge!( + type: 'text', + maxlength: 255, + regex: object_attribte.regexp_for_validation, + ) + end + + private + + def data_type(_attribute) + 'input' + end + end + end + end +end diff --git a/lib/import/zendesk/object_attribute/select.rb b/lib/import/zendesk/object_attribute/select.rb new file mode 100644 index 000000000..f557aca68 --- /dev/null +++ b/lib/import/zendesk/object_attribute/select.rb @@ -0,0 +1,28 @@ +module Import + module Zendesk + class ObjectAttribute + class Select < Import::Zendesk::ObjectAttribute + def init_callback(object_attribte) + @data_option.merge!( + default: '', + options: options(object_attribte), + ) + end + + private + + def data_type(_attribute) + 'select' + end + + def options(object_attribte) + result = {} + object_attribte.custom_field_options.each { |entry| + result[ entry['value'] ] = entry['name'] + } + result + end + end + end + end +end diff --git a/lib/import/zendesk/object_attribute/tagger.rb b/lib/import/zendesk/object_attribute/tagger.rb new file mode 100644 index 000000000..c8481b586 --- /dev/null +++ b/lib/import/zendesk/object_attribute/tagger.rb @@ -0,0 +1,8 @@ +module Import + module Zendesk + class ObjectAttribute + class Tagger < Import::Zendesk::ObjectAttribute::Select + end + end + end +end diff --git a/lib/import/zendesk/object_attribute/text.rb b/lib/import/zendesk/object_attribute/text.rb new file mode 100644 index 000000000..3d68dfcbe --- /dev/null +++ b/lib/import/zendesk/object_attribute/text.rb @@ -0,0 +1,20 @@ +module Import + module Zendesk + class ObjectAttribute + class Text < Import::Zendesk::ObjectAttribute + def init_callback(_object_attribte) + @data_option.merge!( + type: 'text', + maxlength: 255, + ) + end + + private + + def data_type(_attribute) + 'input' + end + end + end + end +end diff --git a/lib/import/zendesk/object_attribute/textarea.rb b/lib/import/zendesk/object_attribute/textarea.rb new file mode 100644 index 000000000..f2e6b9013 --- /dev/null +++ b/lib/import/zendesk/object_attribute/textarea.rb @@ -0,0 +1,20 @@ +module Import + module Zendesk + class ObjectAttribute + class Textarea < Import::Zendesk::ObjectAttribute + def init_callback(_object_attribte) + @data_option.merge!( + type: 'textarea', + maxlength: 255, + ) + end + + private + + def data_type(_attribute) + 'input' + end + end + end + end +end diff --git a/lib/import/zendesk/object_field.rb b/lib/import/zendesk/object_field.rb new file mode 100644 index 000000000..ac0024271 --- /dev/null +++ b/lib/import/zendesk/object_field.rb @@ -0,0 +1,39 @@ +module Import + module Zendesk + class ObjectField + + attr_reader :zendesk_id, :id + + def initialize(object_field) + + import(object_field) + + @zendesk_id = object_field.id + @id = local_name(object_field) + end + + private + + def local_name(object_field) + return @local_name if @local_name + @local_name = remote_name(object_field).gsub(/\s/, '_').downcase + end + + def remote_name(object_field) + object_field['key'] # TODO: y?! + end + + def import(object_field) + backend_class(object_field).new(object_name, local_name(object_field), object_field) + end + + def backend_class(object_field) + "Import::Zendesk::ObjectAttribute::#{object_field.type .capitalize}".constantize + end + + def object_name + self.class.name.to_s.sub(/Import::Zendesk::/, '').sub(/Field/, '') + end + end + end +end diff --git a/lib/import/zendesk/organization.rb b/lib/import/zendesk/organization.rb new file mode 100644 index 000000000..961035c29 --- /dev/null +++ b/lib/import/zendesk/organization.rb @@ -0,0 +1,34 @@ +# https://developer.zendesk.com/rest_api/docs/core/organizations +module Import + module Zendesk + class Organization + include Import::Zendesk::Helper + + attr_reader :zendesk_id, :id + + def initialize(organization) + local_organization = ::Organization.create_if_not_exists(local_organization_fields(organization)) + @zendesk_id = organization.id + @id = local_organization.id + end + + private + + def local_organization_fields(organization) + { + name: organization.name, + note: organization.note, + shared: organization.shared_tickets, + # shared: organization.shared_comments, # TODO, not yet implemented + # }.merge(organization.organization_fields) # TODO + updated_by_id: 1, + created_by_id: 1 + }.merge(custom_fields(organization)) + end + + def custom_fields(organization) + get_fields(organization.organization_fields) + end + end + end +end diff --git a/lib/import/zendesk/organization_factory.rb b/lib/import/zendesk/organization_factory.rb new file mode 100644 index 000000000..5f1af7584 --- /dev/null +++ b/lib/import/zendesk/organization_factory.rb @@ -0,0 +1,10 @@ +module Import + module Zendesk + module OrganizationFactory + # we need to loop over each instead of all! + # so we can use the default import factory here + extend Import::Factory + extend Import::Zendesk::LocalIDMapperHook + end + end +end diff --git a/lib/import/zendesk/organization_field.rb b/lib/import/zendesk/organization_field.rb new file mode 100644 index 000000000..f29fb53ef --- /dev/null +++ b/lib/import/zendesk/organization_field.rb @@ -0,0 +1,6 @@ +module Import + module Zendesk + class OrganizationField < Import::Zendesk::ObjectField + end + end +end diff --git a/lib/import/zendesk/organization_field_factory.rb b/lib/import/zendesk/organization_field_factory.rb new file mode 100644 index 000000000..cf235422b --- /dev/null +++ b/lib/import/zendesk/organization_field_factory.rb @@ -0,0 +1,8 @@ +module Import + module Zendesk + module OrganizationFieldFactory + extend Import::Zendesk::BaseFactory + extend Import::Zendesk::LocalIDMapperHook + end + end +end diff --git a/lib/import/zendesk/priority.rb b/lib/import/zendesk/priority.rb new file mode 100644 index 000000000..25b7e9c44 --- /dev/null +++ b/lib/import/zendesk/priority.rb @@ -0,0 +1,32 @@ +module Import + module Zendesk + class Priority + + MAPPING = { + 'low' => '1 low', + nil => '2 normal', + 'normal' => '2 normal', + 'high' => '3 high', + 'urgent' => '3 high', + }.freeze + + class << self + + def lookup(ticket) + remote_priority = ticket.priority + @mapping ||= {} + if @mapping[ remote_priority ] + return @mapping[ remote_priority ] + end + @mapping[ remote_priority ] = ::Ticket::Priority.lookup( name: map(remote_priority) ) + end + + private + + def map(priority) + MAPPING.fetch(priority, MAPPING[nil]) + end + end + end + end +end diff --git a/lib/import/zendesk/requester.rb b/lib/import/zendesk/requester.rb new file mode 100644 index 000000000..7fd433b1d --- /dev/null +++ b/lib/import/zendesk/requester.rb @@ -0,0 +1,39 @@ +module Import + module Zendesk + module Requester + + # rubocop:disable Style/ModuleFunction + extend self + + def connection_test + # make sure to reinitialize client + # to react to config changes + initialize_client + return true if client.users.first + false + end + + def client + return @client if @client + initialize_client + @client + end + + private + + def initialize_client + @client = ZendeskAPI::Client.new do |config| + config.url = Setting.get('import_zendesk_endpoint') + + # Basic / Token Authentication + config.username = Setting.get('import_zendesk_endpoint_username') + config.token = Setting.get('import_zendesk_endpoint_key') + + # when hitting the rate limit, sleep automatically, + # then retry the request. + config.retry = true + end + end + end + end +end diff --git a/lib/import/zendesk/state.rb b/lib/import/zendesk/state.rb new file mode 100644 index 000000000..25fabd497 --- /dev/null +++ b/lib/import/zendesk/state.rb @@ -0,0 +1,29 @@ +module Import + module Zendesk + class State + + MAPPING = { + 'pending' => 'pending reminder', + 'solved' => 'closed', + }.freeze + + class << self + + def lookup(ticket) + remote_state = ticket.status + @mapping ||= {} + if @mapping[ remote_state ] + return @mapping[ remote_state ] + end + @mapping[ remote_state ] = ::Ticket::State.lookup( name: map( remote_state ) ) + end + + private + + def map(state) + MAPPING.fetch(state, state) + end + end + end + end +end diff --git a/lib/import/zendesk/ticket.rb b/lib/import/zendesk/ticket.rb new file mode 100644 index 000000000..bbebfe2e7 --- /dev/null +++ b/lib/import/zendesk/ticket.rb @@ -0,0 +1,73 @@ +# https://developer.zendesk.com/rest_api/docs/core/tickets +# https://developer.zendesk.com/rest_api/docs/core/ticket_comments#ticket-comments +# https://developer.zendesk.com/rest_api/docs/core/ticket_audits#the-via-object +# https://developer.zendesk.com/rest_api/docs/help_center/article_attachments +# https://developer.zendesk.com/rest_api/docs/core/ticket_audits # v2 +module Import + module Zendesk + class Ticket + include Import::Helper + + def initialize(ticket) + create_or_update(ticket) + Import::Zendesk::Ticket::TagFactory.import(ticket.tags, @local_ticket, ticket) + Import::Zendesk::Ticket::CommentFactory.import(ticket.comments, @local_ticket, ticket) + end + + private + + def create_or_update(ticket) + mapped_ticket = local_ticket_fields(ticket) + return if updated?(mapped_ticket) + create(mapped_ticket) + end + + def updated?(ticket) + @local_ticket = ::Ticket.find_by(id: ticket[:id]) + return false if !@local_ticket + @local_ticket.update_attributes(ticket) + true + end + + def create(ticket) + @local_ticket = ::Ticket.create(ticket) + reset_primary_key_sequence('tickets') + end + + def local_ticket_fields(ticket) + local_user_id = Import::Zendesk::UserFactory.local_id( ticket.requester_id ) || 1 + + { + id: ticket.id, + title: ticket.subject, + note: ticket.description, + group_id: Import::Zendesk::GroupFactory.local_id( ticket.group_id ) || 1, + customer_id: local_user_id, + organization_id: Import::Zendesk::OrganizationFactory.local_id( ticket.organization_id ), + priority: Import::Zendesk::Priority.lookup(ticket), + state: Import::Zendesk::State.lookup(ticket), + pending_time: ticket.due_at, + updated_at: ticket.updated_at, + created_at: ticket.created_at, + updated_by_id: local_user_id, + created_by_id: local_user_id, + create_article_sender_id: Import::Zendesk::Ticket::Comment::Sender.local_id(local_user_id), + create_article_type_id: Import::Zendesk::Ticket::Comment::Type.local_id(ticket), + }.merge(custom_fields(ticket)) + end + + def custom_fields(ticket) + custom_fields = ticket.custom_fields + fields = {} + return fields if !custom_fields + custom_fields.each do |custom_field| + field_name = Import::Zendesk::TicketFieldFactory.local_id(custom_field['id']) + field_value = custom_field['value'] + next if field_value.nil? + fields[ field_name.to_sym ] = field_value + end + fields + end + end + end +end diff --git a/lib/import/zendesk/ticket/comment.rb b/lib/import/zendesk/ticket/comment.rb new file mode 100644 index 000000000..d6a2f3fff --- /dev/null +++ b/lib/import/zendesk/ticket/comment.rb @@ -0,0 +1,71 @@ +module Import + module Zendesk + class Ticket + class Comment + + def initialize(comment, local_ticket, _zendesk_ticket) + create_or_update(comment, local_ticket) + import_attachments(comment) + end + + private + + def create_or_update(comment, local_ticket) + mapped_article = local_article_fields(comment, local_ticket) + return if updated?(mapped_article) + create(mapped_article) + end + + def updated?(article) + @local_article = ::Ticket::Article.find_by(message_id: article[:message_id]) + return false if !@local_article + @local_article.update_attributes(article) + true + end + + def create(article) + @local_article = ::Ticket::Article.create(article) + end + + def local_article_fields(comment, local_ticket) + + local_user_id = Import::Zendesk::UserFactory.local_id( comment.author_id ) || 1 + + { + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: Import::Zendesk::Ticket::Comment::Sender.local_id( local_user_id ), + type_id: Import::Zendesk::Ticket::Comment::Type.local_id(comment), + }.merge(from_to(comment)) + end + + def from_to(comment) + if comment.via.channel == 'email' + { + from: comment.via.source.from.address, + to: comment.via.source.to.address # Notice comment.via.from.original_recipients = [\"another@gmail.com\", \"support@example.zendesk.com\"] + } + elsif comment.via.channel == 'facebook' + { + from: comment.via.source.from.facebook_id, + to: comment.via.source.to.facebook_id + } + else + {} + end + end + + def import_attachments(comment) + attachments = comment.attachments + return if attachments.empty? + Import::Zendesk::Ticket::Comment::AttachmentFactory.import(attachments, @local_article) + end + end + end + end +end diff --git a/lib/import/zendesk/ticket/comment/attachment.rb b/lib/import/zendesk/ticket/comment/attachment.rb new file mode 100644 index 000000000..9c1ec687a --- /dev/null +++ b/lib/import/zendesk/ticket/comment/attachment.rb @@ -0,0 +1,43 @@ +module Import + module Zendesk + class Ticket + class Comment + class Attachment + extend Import::Helper + + def initialize(attachment, local_article) + + response = request(attachment) + return if !response + + ::Store.add( + object: 'Ticket::Article', + o_id: local_article.id, + data: response.body, + filename: attachment.file_name, + preferences: { + 'Content-Type' => attachment.content_type + }, + created_by_id: 1 + ) + end + + private + + def request(attachment) + response = UserAgent.get( + attachment.content_url, + {}, + { + open_timeout: 10, + read_timeout: 60, + }, + ) + return response if response.success? + log response.error + end + end + end + end + end +end diff --git a/lib/import/zendesk/ticket/comment/attachment_factory.rb b/lib/import/zendesk/ticket/comment/attachment_factory.rb new file mode 100644 index 000000000..474e21501 --- /dev/null +++ b/lib/import/zendesk/ticket/comment/attachment_factory.rb @@ -0,0 +1,20 @@ +module Import + module Zendesk + class Ticket + class Comment + module AttachmentFactory + # we need to loop over each instead of all! + # so we can use the default import factory here + extend Import::Factory + + private + + def create_instance(record, *args) + local_article = args[0] + backend_class(record).new(record, local_article) + end + end + end + end + end +end diff --git a/lib/import/zendesk/ticket/comment/sender.rb b/lib/import/zendesk/ticket/comment/sender.rb new file mode 100644 index 000000000..f8d3ab028 --- /dev/null +++ b/lib/import/zendesk/ticket/comment/sender.rb @@ -0,0 +1,49 @@ +module Import + module Zendesk + class Ticket + class Comment + module Sender + + # rubocop:disable Style/ModuleFunction + extend self + + def local_id(user_id) + author = author_lookup(user_id) + sender_id(author) + end + + private + + def author_lookup(user_id) + ::User.find( user_id ) + end + + def sender_id(author) + if author.role?('Customer') + article_sender_customer + elsif author.role?('Agent') + article_sender_agent + else + article_sender_system + end + end + + def article_sender_customer + return @article_sender_customer if @article_sender_customer + @article_sender_customer = ::Ticket::Article::Sender.lookup(name: 'Customer').id + end + + def article_sender_agent + return @article_sender_agent if @article_sender_agent + @article_sender_agent = ::Ticket::Article::Sender.lookup(name: 'Agent').id + end + + def article_sender_system + return @article_sender_system if @article_sender_system + @article_sender_system = ::Ticket::Article::Sender.lookup(name: 'System').id + end + end + end + end + end +end diff --git a/lib/import/zendesk/ticket/comment/type.rb b/lib/import/zendesk/ticket/comment/type.rb new file mode 100644 index 000000000..8dad31905 --- /dev/null +++ b/lib/import/zendesk/ticket/comment/type.rb @@ -0,0 +1,60 @@ +module Import + module Zendesk + class Ticket + class Comment + module Type + + # rubocop:disable Style/ModuleFunction + extend self + + def local_id(object) + case object.via.channel + when 'web' + article_type_id[:web] + when 'email' + article_type_id[:email] + when 'sample_ticket' + article_type_id[:note] + when 'twitter' + if object.via.source.rel == 'mention' + article_type_id[:twitter_status] + else + article_type_id[:twitter_direct_message] + end + when 'facebook' + if object.via.source.rel == 'post' + article_type_id[:facebook_feed_post] + else + article_type_id[:facebook_feed_comment] + end + # fallback for other not (yet) supported article types + # See: + # https://support.zendesk.com/hc/en-us/articles/203661746-Zendesk-Glossary#topic_zie_aqe_tf + # https://support.zendesk.com/hc/en-us/articles/203661596-About-Zendesk-Support-channels + else + article_type_id[:web] + end + end + + private + + def article_type_id + return @article_type_id if @article_type_id + + article_types = ['web', 'note', 'email', 'twitter status', + 'twitter direct-message', 'facebook feed post', + 'facebook feed comment'] + @article_type_id = {} + article_types.each do |article_type| + + article_type_key = article_type.gsub(/\s|\-/, '_').to_sym + + @article_type_id[article_type_key] = ::Ticket::Article::Type.lookup(name: article_type).id + end + @article_type_id + end + end + end + end + end +end diff --git a/lib/import/zendesk/ticket/comment_factory.rb b/lib/import/zendesk/ticket/comment_factory.rb new file mode 100644 index 000000000..99f96429d --- /dev/null +++ b/lib/import/zendesk/ticket/comment_factory.rb @@ -0,0 +1,9 @@ +module Import + module Zendesk + class Ticket + module CommentFactory + extend Import::Zendesk::Ticket::SubObjectFactory + end + end + end +end diff --git a/lib/import/zendesk/ticket/sub_object_factory.rb b/lib/import/zendesk/ticket/sub_object_factory.rb new file mode 100644 index 000000000..aa67e0861 --- /dev/null +++ b/lib/import/zendesk/ticket/sub_object_factory.rb @@ -0,0 +1,21 @@ +module Import + module Zendesk + class Ticket + module SubObjectFactory + # we need to loop over each instead of all! + # so we can use the default import factory here + include Import::Factory + + private + + def create_instance(record, *args) + + local_ticket = args[0] + zendesk_ticket = args[1] + + backend_class(record).new(record, local_ticket, zendesk_ticket) + end + end + end + end +end diff --git a/lib/import/zendesk/ticket/tag.rb b/lib/import/zendesk/ticket/tag.rb new file mode 100644 index 000000000..f4a07a079 --- /dev/null +++ b/lib/import/zendesk/ticket/tag.rb @@ -0,0 +1,16 @@ +module Import + module Zendesk + class Ticket + class Tag + def initialize(tag, local_ticket, zendesk_ticket) + ::Tag.tag_add( + object: 'Ticket', + o_id: local_ticket.id, + item: tag.id, + created_by_id: Import::Zendesk::UserFactory.local_id(zendesk_ticket.requester_id) || 1, + ) + end + end + end + end +end diff --git a/lib/import/zendesk/ticket/tag_factory.rb b/lib/import/zendesk/ticket/tag_factory.rb new file mode 100644 index 000000000..7fed1c5d8 --- /dev/null +++ b/lib/import/zendesk/ticket/tag_factory.rb @@ -0,0 +1,9 @@ +module Import + module Zendesk + class Ticket + module TagFactory + extend Import::Zendesk::Ticket::SubObjectFactory + end + end + end +end diff --git a/lib/import/zendesk/ticket_factory.rb b/lib/import/zendesk/ticket_factory.rb new file mode 100644 index 000000000..0362f1f41 --- /dev/null +++ b/lib/import/zendesk/ticket_factory.rb @@ -0,0 +1,7 @@ +module Import + module Zendesk + module TicketFactory + extend Import::Zendesk::BaseFactory + end + end +end diff --git a/lib/import/zendesk/ticket_field.rb b/lib/import/zendesk/ticket_field.rb new file mode 100644 index 000000000..1a84bd374 --- /dev/null +++ b/lib/import/zendesk/ticket_field.rb @@ -0,0 +1,12 @@ +module Import + module Zendesk + class TicketField < Import::Zendesk::ObjectField + + private + + def remote_name(ticket_field) + ticket_field.title + end + end + end +end diff --git a/lib/import/zendesk/ticket_field_factory.rb b/lib/import/zendesk/ticket_field_factory.rb new file mode 100644 index 000000000..b85dd8eb5 --- /dev/null +++ b/lib/import/zendesk/ticket_field_factory.rb @@ -0,0 +1,33 @@ +module Import + module Zendesk + module TicketFieldFactory + extend Import::Zendesk::BaseFactory + extend Import::Zendesk::LocalIDMapperHook + + MAPPING = { + 'subject' => 'title', + 'description' => 'note', + 'status' => 'state_id', + 'tickettype' => 'type', + 'priority' => 'priority_id', + 'group' => 'group_id', + 'assignee' => 'owner_id', + }.freeze + + # rubocop:disable Style/ModuleFunction + extend self + + def skip?(field) + # check if the Ticket object already has a same named column / attribute + # so we want to skip instead of importing it + Ticket.column_names.include?( local_attribute(field) ) + end + + private + + def local_attribute(field) + MAPPING.fetch(field.type, field.type) + end + end + end +end diff --git a/lib/import/zendesk/user.rb b/lib/import/zendesk/user.rb new file mode 100644 index 000000000..bda0b4d8c --- /dev/null +++ b/lib/import/zendesk/user.rb @@ -0,0 +1,75 @@ +# Rails autoload has some issues with same namend sub-classes +# in the importer folder require AND simultaniuos requiring +# of the same file in different threads so we need to +# require them ourself +require 'import/zendesk/user/group' +require 'import/zendesk/user/role' + +# https://developer.zendesk.com/rest_api/docs/core/users +module Import + module Zendesk + class User + include Import::Zendesk::Helper + + attr_reader :zendesk_id, :id + + def initialize(user) + local_user = ::User.create_or_update( local_user_fields(user) ) + @zendesk_id = user.id + @id = local_user.id + end + + private + + def local_user_fields(user) + { + login: login(user), + firstname: user.name, + email: user.email, + phone: user.phone, + password: password(user), + active: !user.suspended, + groups: Import::Zendesk::User::Group.for(user), + roles: roles(user), + note: user.notes, + verified: user.verified, + organization_id: Import::Zendesk::OrganizationFactory.local_id( user.organization_id ), + last_login: user.last_login_at, + image_source: photo(user), + updated_by_id: 1, + created_by_id: 1 + }.merge(custom_fields(user)) + end + + def login(user) + return user.email if user.email + # Zendesk users may have no other identifier than the ID, e.g. twitter users + user.id.to_s + end + + def password(user) + return Setting.get('import_zendesk_endpoint_key') if import_user?(user) + '' + end + + def roles(user) + return Import::Zendesk::User::Role.map(user, 'admin') if import_user?(user) + Import::Zendesk::User::Role.for(user) + end + + def import_user?(user) + return false if user.email.blank? + user.email == Setting.get('import_zendesk_endpoint_username') + end + + def photo(user) + return if !user.photo + user.photo.content_url + end + + def custom_fields(user) + get_fields(user.user_fields) + end + end + end +end diff --git a/lib/import/zendesk/user/group.rb b/lib/import/zendesk/user/group.rb new file mode 100644 index 000000000..0fe82e247 --- /dev/null +++ b/lib/import/zendesk/user/group.rb @@ -0,0 +1,51 @@ +# this require is required (hehe) because of Rails autoloading +# which causes strange behavior not inheriting correctly +# from Import::OTRS::DynamicField +require 'import/zendesk/user' + +# https://developer.zendesk.com/rest_api/docs/core/groups +module Import + module Zendesk + class User + module Group + + # rubocop:disable Style/ModuleFunction + extend self + + def for(user) + groups = [] + return groups if mapping[user.id].empty? + + mapping[user.id].each { |zendesk_group_id| + + local_group_id = Import::Zendesk::GroupFactory.local_id(zendesk_group_id) + + next if !local_group_id + + group = ::Group.find( local_group_id ) + + groups.push(group) + } + groups + end + + private + + def mapping + + return @mapping if !@mapping.nil? + + @mapping = {} + + Import::Zendesk::Requester.client.group_memberships.all! { |group_membership| + + @mapping[ group_membership.user_id ] ||= [] + @mapping[ group_membership.user_id ].push( group_membership.group_id ) + } + + @mapping + end + end + end + end +end diff --git a/lib/import/zendesk/user/role.rb b/lib/import/zendesk/user/role.rb new file mode 100644 index 000000000..e5b389b47 --- /dev/null +++ b/lib/import/zendesk/user/role.rb @@ -0,0 +1,62 @@ +# this require is required (hehe) because of Rails autoloading +# which causes strange behavior not inheriting correctly +# from Import::OTRS::DynamicField +require 'import/zendesk/user' + +module Import + module Zendesk + class User + module Role + extend Import::Helper + + # rubocop:disable Style/ModuleFunction + extend self + + def for(user) + map(user, group_method( user.role.name )) + end + + def map(user, role) + send(role.to_sym, user) + rescue NoMethodError => e + log "Unknown mapping for role '#{user.role.name}' and user with id '#{user.id}'" + [] + end + + private + + def end_user(_user) + [role_customer] + end + + def agent(user) + return [ role_agent ] if user.restricted_agent + admin(user) + end + + def admin(_user) + [role_admin, role_agent] + end + + def group_method(role) + role.tr('-', '_') + end + + def role_admin + return @role_admin if @role_admin + @role_admin = ::Role.lookup(name: 'Admin') + end + + def role_agent + return @role_agent if @role_agent + @role_agent = ::Role.lookup(name: 'Agent') + end + + def role_customer + return @role_customer if @role_customer + @role_customer = ::Role.lookup(name: 'Customer') + end + end + end + end +end diff --git a/lib/import/zendesk/user_factory.rb b/lib/import/zendesk/user_factory.rb new file mode 100644 index 000000000..ec3fafe5b --- /dev/null +++ b/lib/import/zendesk/user_factory.rb @@ -0,0 +1,8 @@ +module Import + module Zendesk + module UserFactory + extend Import::Zendesk::BaseFactory + extend Import::Zendesk::LocalIDMapperHook + end + end +end diff --git a/lib/import/zendesk/user_field.rb b/lib/import/zendesk/user_field.rb new file mode 100644 index 000000000..5b8f615f1 --- /dev/null +++ b/lib/import/zendesk/user_field.rb @@ -0,0 +1,6 @@ +module Import + module Zendesk + class UserField < Import::Zendesk::ObjectField + end + end +end diff --git a/lib/import/zendesk/user_field_factory.rb b/lib/import/zendesk/user_field_factory.rb new file mode 100644 index 000000000..101d50178 --- /dev/null +++ b/lib/import/zendesk/user_field_factory.rb @@ -0,0 +1,8 @@ +module Import + module Zendesk + module UserFieldFactory + extend Import::Zendesk::BaseFactory + extend Import::Zendesk::LocalIDMapperHook + end + end +end diff --git a/spec/import/otrs/async_examples.rb b/spec/import/async_examples.rb similarity index 80% rename from spec/import/otrs/async_examples.rb rename to spec/import/async_examples.rb index 4ae347102..974d8b63f 100644 --- a/spec/import/otrs/async_examples.rb +++ b/spec/import/async_examples.rb @@ -1,4 +1,4 @@ -RSpec.shared_examples 'Import::OTRS::Async' do +RSpec.shared_examples 'Import::Async' do it 'responds to start_bg' do expect(described_class).to respond_to('start_bg') end diff --git a/spec/import/base_factory_examples.rb b/spec/import/base_factory_examples.rb index ca15fafcd..173644dcc 100644 --- a/spec/import/base_factory_examples.rb +++ b/spec/import/base_factory_examples.rb @@ -6,6 +6,9 @@ RSpec.shared_examples 'Import::BaseFactory' do it 'responds to pre_import_hook' do expect(described_class).to respond_to('pre_import_hook') end + it 'responds to post_import_hook' do + expect(described_class).to respond_to('post_import_hook') + end it 'responds to backend_class' do expect(described_class).to respond_to('backend_class') end @@ -18,7 +21,7 @@ RSpec.shared_examples 'Import::BaseFactory extender' do it 'calls new on determined backend object' do record = double() expect(described_class).to receive(:backend_class).and_return(Class) - expect(Class).to receive(:new).with(record) + expect(Class).to receive(:new).with(record, any_args) described_class.import([record]) end end diff --git a/spec/import/otrs/import_stats_examples.rb b/spec/import/import_stats_examples.rb similarity index 78% rename from spec/import/otrs/import_stats_examples.rb rename to spec/import/import_stats_examples.rb index 7cbb7ea59..dc08268ca 100644 --- a/spec/import/otrs/import_stats_examples.rb +++ b/spec/import/import_stats_examples.rb @@ -1,4 +1,4 @@ -RSpec.shared_examples 'Import::OTRS::ImportStats' do +RSpec.shared_examples 'Import::ImportStats' do it 'responds to current_state' do expect(described_class).to respond_to('current_state') end diff --git a/spec/import/otrs/priority_factory_spec.rb b/spec/import/otrs/priority_factory_spec.rb index 34033008c..0f3d82288 100644 --- a/spec/import/otrs/priority_factory_spec.rb +++ b/spec/import/otrs/priority_factory_spec.rb @@ -3,4 +3,13 @@ require 'import/factory_examples' RSpec.describe Import::OTRS::PriorityFactory do it_behaves_like 'Import::Factory' + + it 'imports records' do + + import_data = { + name: 'test', + } + expect(::Import::OTRS::Priority).to receive(:new).with(import_data) + described_class.import([import_data]) + end end diff --git a/spec/import/otrs_spec.rb b/spec/import/otrs_spec.rb index 95bb5a10f..efe89745e 100644 --- a/spec/import/otrs_spec.rb +++ b/spec/import/otrs_spec.rb @@ -1,14 +1,14 @@ require 'rails_helper' require 'import/helper_examples' require 'import/importer_examples' -require 'import/otrs/async_examples' require 'import/otrs/diff_examples' -require 'import/otrs/import_stats_examples' +require 'import/async_examples' +require 'import/import_stats_examples' RSpec.describe Import::OTRS do it_behaves_like 'Import backend' + it_behaves_like 'Import::Async' it_behaves_like 'Import::Helper' - it_behaves_like 'Import::OTRS::Async' + it_behaves_like 'Import::ImportStats' it_behaves_like 'Import::OTRS::Diff' - it_behaves_like 'Import::OTRS::ImportStats' end diff --git a/spec/import/zendesk/base_factory_examples.rb b/spec/import/zendesk/base_factory_examples.rb new file mode 100644 index 000000000..adae42f6d --- /dev/null +++ b/spec/import/zendesk/base_factory_examples.rb @@ -0,0 +1,23 @@ +require 'import/factory_examples' + +RSpec.shared_examples 'Import::Zendesk::BaseFactory' do + it_behaves_like 'Import::Factory' + + it 'calls .all! on parameter object' do + parameter = double() + expect(parameter).to receive('all!') + described_class.import(parameter) + end + + it 'calls new on determined backend object' do + expect(described_class).to receive(:backend_class).and_return(Class) + expect(described_class).to receive('skip?') + expect(described_class).to receive(:pre_import_hook) + expect(described_class).to receive(:post_import_hook) + record = double() + expect(Class).to receive(:new).with(record, any_args) + parameter = double() + expect(parameter).to receive('all!').and_yield(record) + described_class.import(parameter) + end +end diff --git a/spec/import/zendesk/base_factory_spec.rb b/spec/import/zendesk/base_factory_spec.rb new file mode 100644 index 000000000..18a6cc412 --- /dev/null +++ b/spec/import/zendesk/base_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/base_factory_examples' + +RSpec.describe Import::Zendesk::BaseFactory do + it_behaves_like 'Import::Zendesk::BaseFactory' +end diff --git a/spec/import/zendesk/group_factory_spec.rb b/spec/import/zendesk/group_factory_spec.rb new file mode 100644 index 000000000..3ab082675 --- /dev/null +++ b/spec/import/zendesk/group_factory_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'import/zendesk/base_factory_examples' +require 'import/zendesk/local_id_mapper_hook_examples' + +RSpec.describe Import::Zendesk::GroupFactory do + it_behaves_like 'Import::Zendesk::BaseFactory' + it_behaves_like 'Import::Zendesk::LocalIDMapperHook' +end diff --git a/spec/import/zendesk/group_spec.rb b/spec/import/zendesk/group_spec.rb new file mode 100644 index 000000000..014f0e405 --- /dev/null +++ b/spec/import/zendesk/group_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::Group do + + it 'creates a group if not exists' do + + group = double( + id: 31_337, + name: 'Test Group', + deleted: true + ) + + local_group = instance_double(::Group, id: 1337) + + expect(::Group).to receive(:create_if_not_exists).with(hash_including(name: group.name, active: !group.deleted)).and_return(local_group) + + created_instance = described_class.new(group) + + expect(created_instance).to respond_to(:id) + expect(created_instance.id).to eq(local_group.id) + + expect(created_instance).to respond_to(:zendesk_id) + expect(created_instance.zendesk_id).to eq(group.id) + end +end diff --git a/spec/import/zendesk/local_id_mapper_hook_examples.rb b/spec/import/zendesk/local_id_mapper_hook_examples.rb new file mode 100644 index 000000000..27dfddfe8 --- /dev/null +++ b/spec/import/zendesk/local_id_mapper_hook_examples.rb @@ -0,0 +1,20 @@ +RSpec.shared_examples 'Import::Zendesk::LocalIDMapperHook' do + + it 'responds to local_id' do + expect(described_class).to respond_to('local_id') + end + + it 'responds to post_import_hook' do + expect(described_class).to respond_to('post_import_hook') + end + + it 'stores an ID mapping and makes it accessable' do + backend_instance = double( + zendesk_id: 31_337, + id: 1337, + ) + + described_class.post_import_hook(nil, backend_instance) + expect(described_class.local_id(backend_instance.zendesk_id)).to eq(backend_instance.id) + end +end diff --git a/spec/import/zendesk/local_id_mapper_hook_spec.rb b/spec/import/zendesk/local_id_mapper_hook_spec.rb new file mode 100644 index 000000000..8d527a67c --- /dev/null +++ b/spec/import/zendesk/local_id_mapper_hook_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/local_id_mapper_hook_examples' + +RSpec.describe Import::Zendesk::LocalIDMapperHook do + it_behaves_like 'Import::Zendesk::LocalIDMapperHook' +end diff --git a/spec/import/zendesk/lookup_backend_examples.rb b/spec/import/zendesk/lookup_backend_examples.rb new file mode 100644 index 000000000..f1e8220ff --- /dev/null +++ b/spec/import/zendesk/lookup_backend_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'Lookup backend' do + it 'responds to lookup' do + expect(described_class).to respond_to('lookup') + end +end diff --git a/spec/import/zendesk/object_attribute/checkbox_spec.rb b/spec/import/zendesk/object_attribute/checkbox_spec.rb new file mode 100644 index 000000000..be31043b0 --- /dev/null +++ b/spec/import/zendesk/object_attribute/checkbox_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::ObjectAttribute::Checkbox do + + it 'imports boolean object attribute from checkbox object field' do + + attribute = double( + title: 'Example attribute', + description: 'Example attribute description', + removable: false, + active: true, + position: 12, + visible_in_portal: true, + required_in_portal: true, + required: true, + type: 'checkbox', + ) + + expected_structure = { + object: 'Ticket', + name: 'example_field', + display: 'Example attribute', + data_type: 'boolean', + data_option: { + null: false, + note: 'Example attribute description', + default: false, + options: { + true => 'yes', + false => 'no' + } + }, + editable: true, + active: true, + screens: { + edit: { + Customer: { + shown: true, + null: false + }, + view: { + '-all-' => { + shown: true + } + } + } + }, + position: 12, + created_by_id: 1, + updated_by_id: 1 + } + + expect(ObjectManager::Attribute).to receive(:add).with(expected_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + + described_class.new('Ticket', 'example_field', attribute) + end +end diff --git a/spec/import/zendesk/object_attribute/date_spec.rb b/spec/import/zendesk/object_attribute/date_spec.rb new file mode 100644 index 000000000..fe19ba525 --- /dev/null +++ b/spec/import/zendesk/object_attribute/date_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +# required due to some of rails autoloading issues +require 'import/zendesk/object_attribute/date' + +RSpec.describe Import::Zendesk::ObjectAttribute::Date do + + it 'imports date object attribute from date object field' do + + attribute = double( + title: 'Example attribute', + description: 'Example attribute description', + removable: false, + active: true, + position: 12, + visible_in_portal: true, + required_in_portal: true, + required: true, + type: 'date', + ) + + expected_structure = { + object: 'Ticket', + name: 'example_field', + display: 'Example attribute', + data_type: 'date', + data_option: { + null: false, + note: 'Example attribute description', + future: true, + past: true, + diff: 0, + }, + editable: true, + active: true, + screens: { + edit: { + Customer: { + shown: true, + null: false + }, + view: { + '-all-' => { + shown: true + } + } + } + }, + position: 12, + created_by_id: 1, + updated_by_id: 1 + } + + expect(ObjectManager::Attribute).to receive(:add).with(expected_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + + described_class.new('Ticket', 'example_field', attribute) + end +end diff --git a/spec/import/zendesk/object_attribute/decimal_spec.rb b/spec/import/zendesk/object_attribute/decimal_spec.rb new file mode 100644 index 000000000..37014e780 --- /dev/null +++ b/spec/import/zendesk/object_attribute/decimal_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::ObjectAttribute::Decimal do + + it 'imports input object attribute from decimal object field' do + + attribute = double( + title: 'Example attribute', + description: 'Example attribute description', + removable: false, + active: true, + position: 12, + visible_in_portal: true, + required_in_portal: true, + required: true, + type: 'decimal', + ) + + expected_structure = { + object: 'Ticket', + name: 'example_field', + display: 'Example attribute', + data_type: 'input', + data_option: { + null: false, + note: 'Example attribute description', + type: 'text', + maxlength: 255, + }, + editable: true, + active: true, + screens: { + edit: { + Customer: { + shown: true, + null: false + }, + view: { + '-all-' => { + shown: true + } + } + } + }, + position: 12, + created_by_id: 1, + updated_by_id: 1 + } + + expect(ObjectManager::Attribute).to receive(:add).with(expected_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + + described_class.new('Ticket', 'example_field', attribute) + end +end diff --git a/spec/import/zendesk/object_attribute/dropdown_spec.rb b/spec/import/zendesk/object_attribute/dropdown_spec.rb new file mode 100644 index 000000000..6bc10fba9 --- /dev/null +++ b/spec/import/zendesk/object_attribute/dropdown_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::ObjectAttribute::Dropdown do + + it 'imports select object attribute from dropdown object field' do + + attribute = double( + title: 'Example attribute', + description: 'Example attribute description', + removable: false, + active: true, + position: 12, + visible_in_portal: true, + required_in_portal: true, + required: true, + type: 'dropdown', + custom_field_options: [ + { + 'id' => 1, + 'value' => 'Key 1', + 'name' => 'Value 1' + }, + { + 'id' => 2, + 'value' => 'Key 2', + 'name' => 'Value 2' + }, + ] + ) + + expected_structure = { + object: 'Ticket', + name: 'example_field', + display: 'Example attribute', + data_type: 'select', + data_option: { + null: false, + note: 'Example attribute description', + default: '', + options: { + 'Key 1' => 'Value 1', + 'Key 2' => 'Value 2' + }, + }, + editable: true, + active: true, + screens: { + edit: { + Customer: { + shown: true, + null: false + }, + view: { + '-all-' => { + shown: true + } + } + } + }, + position: 12, + created_by_id: 1, + updated_by_id: 1 + } + + expect(ObjectManager::Attribute).to receive(:add).with(expected_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + + created_instance = described_class.new('Ticket', 'example_field', attribute) + end +end diff --git a/spec/import/zendesk/object_attribute/integer_spec.rb b/spec/import/zendesk/object_attribute/integer_spec.rb new file mode 100644 index 000000000..964d1e7a6 --- /dev/null +++ b/spec/import/zendesk/object_attribute/integer_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' + +# required due to some of rails autoloading issues +require 'import/zendesk/object_attribute/integer' + +RSpec.describe Import::Zendesk::ObjectAttribute::Integer do + + it 'imports integer object attribute from integer object field' do + + attribute = double( + title: 'Example attribute', + description: 'Example attribute description', + removable: false, + active: true, + position: 12, + visible_in_portal: true, + required_in_portal: true, + required: true, + type: 'integer', + ) + + expected_structure = { + object: 'Ticket', + name: 'example_field', + display: 'Example attribute', + data_type: 'integer', + data_option: { + null: false, + note: 'Example attribute description', + min: 0, + max: 999_999_999, + }, + editable: true, + active: true, + screens: { + edit: { + Customer: { + shown: true, + null: false + }, + view: { + '-all-' => { + shown: true + } + } + } + }, + position: 12, + created_by_id: 1, + updated_by_id: 1 + } + + expect(ObjectManager::Attribute).to receive(:add).with(expected_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + + described_class.new('Ticket', 'example_field', attribute) + end +end diff --git a/spec/import/zendesk/object_attribute/regexp_spec.rb b/spec/import/zendesk/object_attribute/regexp_spec.rb new file mode 100644 index 000000000..85b30908c --- /dev/null +++ b/spec/import/zendesk/object_attribute/regexp_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +# required due to some of rails autoloading issues +require 'import/zendesk/object_attribute/regexp' + +RSpec.describe Import::Zendesk::ObjectAttribute::Regexp do + + it 'imports input object attribute from regexp object field' do + + regex = '.+?' + attribute = double( + title: 'Example attribute', + description: 'Example attribute description', + removable: false, + active: true, + position: 12, + visible_in_portal: true, + required_in_portal: true, + required: true, + type: 'regexp', + regexp_for_validation: regex + ) + + expected_structure = { + object: 'Ticket', + name: 'example_field', + display: 'Example attribute', + data_type: 'input', + data_option: { + null: false, + note: 'Example attribute description', + type: 'text', + maxlength: 255, + regex: regex, + }, + editable: true, + active: true, + screens: { + edit: { + Customer: { + shown: true, + null: false + }, + view: { + '-all-' => { + shown: true + } + } + } + }, + position: 12, + created_by_id: 1, + updated_by_id: 1 + } + + expect(ObjectManager::Attribute).to receive(:add).with(expected_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + + described_class.new('Ticket', 'example_field', attribute) + end +end diff --git a/spec/import/zendesk/object_attribute/tagger_spec.rb b/spec/import/zendesk/object_attribute/tagger_spec.rb new file mode 100644 index 000000000..f5952f47b --- /dev/null +++ b/spec/import/zendesk/object_attribute/tagger_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::ObjectAttribute::Tagger do + + it 'imports select object attribute from tagger object field' do + + attribute = double( + title: 'Example attribute', + description: 'Example attribute description', + removable: false, + active: true, + position: 12, + visible_in_portal: true, + required_in_portal: true, + required: true, + type: 'tagger', + custom_field_options: [ + { + 'id' => 1, + 'value' => 'Key 1', + 'name' => 'Value 1' + }, + { + 'id' => 2, + 'value' => 'Key 2', + 'name' => 'Value 2' + }, + ] + ) + + expected_structure = { + object: 'Ticket', + name: 'example_field', + display: 'Example attribute', + data_type: 'select', + data_option: { + null: false, + note: 'Example attribute description', + default: '', + options: { + 'Key 1' => 'Value 1', + 'Key 2' => 'Value 2' + }, + }, + editable: true, + active: true, + screens: { + edit: { + Customer: { + shown: true, + null: false + }, + view: { + '-all-' => { + shown: true + } + } + } + }, + position: 12, + created_by_id: 1, + updated_by_id: 1 + } + + expect(ObjectManager::Attribute).to receive(:add).with(expected_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + + created_instance = described_class.new('Ticket', 'example_field', attribute) + end +end diff --git a/spec/import/zendesk/object_attribute/text_spec.rb b/spec/import/zendesk/object_attribute/text_spec.rb new file mode 100644 index 000000000..5648a65a0 --- /dev/null +++ b/spec/import/zendesk/object_attribute/text_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::ObjectAttribute::Text do + + it 'imports input object attribute from text object field' do + + attribute = double( + title: 'Example attribute', + description: 'Example attribute description', + removable: false, + active: true, + position: 12, + visible_in_portal: true, + required_in_portal: true, + required: true, + type: 'text', + ) + + expected_structure = { + object: 'Ticket', + name: 'example_field', + display: 'Example attribute', + data_type: 'input', + data_option: { + null: false, + note: 'Example attribute description', + type: 'text', + maxlength: 255, + }, + editable: true, + active: true, + screens: { + edit: { + Customer: { + shown: true, + null: false + }, + view: { + '-all-' => { + shown: true + } + } + } + }, + position: 12, + created_by_id: 1, + updated_by_id: 1 + } + + expect(ObjectManager::Attribute).to receive(:add).with(expected_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + + described_class.new('Ticket', 'example_field', attribute) + end +end diff --git a/spec/import/zendesk/object_attribute/textarea_spec.rb b/spec/import/zendesk/object_attribute/textarea_spec.rb new file mode 100644 index 000000000..f66209599 --- /dev/null +++ b/spec/import/zendesk/object_attribute/textarea_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::ObjectAttribute::Textarea do + + it 'imports input object attribute from textarea object field' do + + attribute = double( + title: 'Example attribute', + description: 'Example attribute description', + removable: false, + active: true, + position: 12, + visible_in_portal: true, + required_in_portal: true, + required: true, + type: 'textarea', + ) + + expected_structure = { + object: 'Ticket', + name: 'example_field', + display: 'Example attribute', + data_type: 'input', + data_option: { + null: false, + note: 'Example attribute description', + type: 'textarea', + maxlength: 255, + }, + editable: true, + active: true, + screens: { + edit: { + Customer: { + shown: true, + null: false + }, + view: { + '-all-' => { + shown: true + } + } + } + }, + position: 12, + created_by_id: 1, + updated_by_id: 1 + } + + expect(ObjectManager::Attribute).to receive(:add).with(expected_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + + described_class.new('Ticket', 'example_field', attribute) + end +end diff --git a/spec/import/zendesk/object_attribute_spec.rb b/spec/import/zendesk/object_attribute_spec.rb new file mode 100644 index 000000000..4fb38161d --- /dev/null +++ b/spec/import/zendesk/object_attribute_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::ObjectAttribute do + + it 'throws an exception if no init_callback is implemented' do + + attribute = double( + title: 'Example attribute', + description: 'Example attribute description', + removable: false, + active: true, + position: 12, + visible_in_portal: true, + required_in_portal: true, + required: true, + type: 'input', + ) + + expect { described_class.new('Ticket', 'example_field', attribute) }.to raise_error(RuntimeError) + end +end diff --git a/spec/import/zendesk/object_field_examples.rb b/spec/import/zendesk/object_field_examples.rb new file mode 100644 index 000000000..6ad9993d6 --- /dev/null +++ b/spec/import/zendesk/object_field_examples.rb @@ -0,0 +1,22 @@ +RSpec.shared_examples 'Import::Zendesk::ObjectField' do + + it 'initializes a object field backend import' do + + object_field = double(id: 31_337, title: 'Example Field') + allow(object_field).to receive(:[]).with('key').and_return(object_field.title) + + dummy_instance = double() + + local_name = 'example_field' + dummy_backend = instance_double(Class) + expect(dummy_backend).to receive(:new).with(kind_of(String), local_name, object_field).and_return(dummy_instance) + expect_any_instance_of(described_class).to receive(:backend_class).and_return(dummy_backend) + created_instance = described_class.new(object_field) + + expect(created_instance).to respond_to(:id) + expect(created_instance.id).to eq(local_name) + + expect(created_instance).to respond_to(:zendesk_id) + expect(created_instance.zendesk_id).to eq(object_field.id) + end +end diff --git a/spec/import/zendesk/object_field_spec.rb b/spec/import/zendesk/object_field_spec.rb new file mode 100644 index 000000000..6cafe2c96 --- /dev/null +++ b/spec/import/zendesk/object_field_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/object_field_examples' + +RSpec.describe Import::Zendesk::ObjectField do + it_behaves_like 'Import::Zendesk::ObjectField' +end diff --git a/spec/import/zendesk/organization_factory_spec.rb b/spec/import/zendesk/organization_factory_spec.rb new file mode 100644 index 000000000..a3264ee71 --- /dev/null +++ b/spec/import/zendesk/organization_factory_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'import/factory_examples' +require 'import/zendesk/local_id_mapper_hook_examples' + +RSpec.describe Import::Zendesk::OrganizationFieldFactory do + it_behaves_like 'Import::Factory' + it_behaves_like 'Import::Zendesk::LocalIDMapperHook' +end diff --git a/spec/import/zendesk/organization_field_factory_spec.rb b/spec/import/zendesk/organization_field_factory_spec.rb new file mode 100644 index 000000000..78d3eeee3 --- /dev/null +++ b/spec/import/zendesk/organization_field_factory_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'import/zendesk/base_factory_examples' +require 'import/zendesk/local_id_mapper_hook_examples' + +RSpec.describe Import::Zendesk::OrganizationFieldFactory do + it_behaves_like 'Import::Zendesk::BaseFactory' + it_behaves_like 'Import::Zendesk::LocalIDMapperHook' +end diff --git a/spec/import/zendesk/organization_field_spec.rb b/spec/import/zendesk/organization_field_spec.rb new file mode 100644 index 000000000..df323d8c4 --- /dev/null +++ b/spec/import/zendesk/organization_field_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/object_field_examples' + +RSpec.describe Import::Zendesk::OrganizationField do + it_behaves_like 'Import::Zendesk::ObjectField' +end diff --git a/spec/import/zendesk/organization_spec.rb b/spec/import/zendesk/organization_spec.rb new file mode 100644 index 000000000..24ba4f850 --- /dev/null +++ b/spec/import/zendesk/organization_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::Organization do + + it 'creates a organization if not exists' do + + organization = double( + id: 31_337, + name: 'Test Organization', + note: 'Test Organization note', + shared_tickets: true, + organization_fields: nil + ) + + local_organization = instance_double(::Organization, id: 1337) + + expect(::Organization).to receive(:create_if_not_exists).with(hash_including(name: organization.name, note: organization.note, shared: organization.shared_tickets)).and_return(local_organization) + + created_instance = described_class.new(organization) + + expect(created_instance).to respond_to(:id) + expect(created_instance).to respond_to(:zendesk_id) + + expect(created_instance.id).to eq(local_organization.id) + expect(created_instance.zendesk_id).to eq(organization.id) + end +end diff --git a/spec/import/zendesk/priority_spec.rb b/spec/import/zendesk/priority_spec.rb new file mode 100644 index 000000000..5ab940aff --- /dev/null +++ b/spec/import/zendesk/priority_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' +require 'import/zendesk/lookup_backend_examples' + +RSpec.describe Import::Zendesk::Priority do + it_behaves_like 'Lookup backend' + + it 'looks up ticket priority' do + + ticket = double(priority: nil) + dummy_result = 'dummy result' + expect(::Ticket::Priority).to receive(:lookup).and_return(dummy_result) + expect(described_class.lookup(ticket)).to eq(dummy_result) + end +end diff --git a/spec/import/zendesk/state_spec.rb b/spec/import/zendesk/state_spec.rb new file mode 100644 index 000000000..075cb724d --- /dev/null +++ b/spec/import/zendesk/state_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' +require 'import/zendesk/lookup_backend_examples' + +RSpec.describe Import::Zendesk::State do + it_behaves_like 'Lookup backend' + + it 'looks up ticket state' do + + ticket = double(status: nil) + dummy_result = 'dummy result' + expect(::Ticket::State).to receive(:lookup).and_return(dummy_result) + expect(described_class.lookup(ticket)).to eq(dummy_result) + end +end diff --git a/spec/import/zendesk/ticket/comment/attachment_factory_spec.rb b/spec/import/zendesk/ticket/comment/attachment_factory_spec.rb new file mode 100644 index 000000000..6b5daef23 --- /dev/null +++ b/spec/import/zendesk/ticket/comment/attachment_factory_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::Zendesk::Ticket::Comment::AttachmentFactory do + it_behaves_like 'Import::Factory' + + it 'tunnels attachment and local article to backend' do + expect(described_class).to receive(:backend_class).and_return(Class) + expect(described_class).to receive('skip?') + expect(described_class).to receive(:pre_import_hook) + expect(described_class).to receive(:post_import_hook) + record = double() + local_article = double() + expect(Class).to receive(:new).with(record, local_article) + parameter = double() + expect(parameter).to receive(:each).and_yield(record) + described_class.import(parameter, local_article) + end +end diff --git a/spec/import/zendesk/ticket/comment/attachment_spec.rb b/spec/import/zendesk/ticket/comment/attachment_spec.rb new file mode 100644 index 000000000..5376e8059 --- /dev/null +++ b/spec/import/zendesk/ticket/comment/attachment_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::Ticket::Comment::Attachment do + + it 'downloads and stores attachments' do + + local_article = double(id: 1337) + + attachment = double( + file_name: 'Example.zip', + content_type: 'application/zip', + content_url: 'https://dl.remote.tld/394r0eifwskfjwlw3slf' + ) + + response = double( + body: 'content', + success?: true, + ) + + expect(UserAgent).to receive(:get).with(attachment.content_url, any_args).and_return(response) + + add_args = { + object: 'Ticket::Article', + o_id: local_article.id, + data: response.body, + filename: attachment.file_name, + preferences: { + 'Content-Type' => attachment.content_type + }, + created_by_id: 1 + } + + expect(Store).to receive(:add).with(add_args) + + described_class.new(attachment, local_article) + end +end diff --git a/spec/import/zendesk/ticket/comment/local_id_lookup_backend_examples.rb b/spec/import/zendesk/ticket/comment/local_id_lookup_backend_examples.rb new file mode 100644 index 000000000..254eaaa9b --- /dev/null +++ b/spec/import/zendesk/ticket/comment/local_id_lookup_backend_examples.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +RSpec.shared_examples 'local_id lookup backend' do + it 'responds to local_id' do + expect(described_class).to respond_to(:local_id) + end +end diff --git a/spec/import/zendesk/ticket/comment/sender_spec.rb b/spec/import/zendesk/ticket/comment/sender_spec.rb new file mode 100644 index 000000000..283359857 --- /dev/null +++ b/spec/import/zendesk/ticket/comment/sender_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/ticket/comment/local_id_lookup_backend_examples' + +RSpec.describe Import::Zendesk::Ticket::Comment::Sender do + it_behaves_like 'local_id lookup backend' +end diff --git a/spec/import/zendesk/ticket/comment/type_spec.rb b/spec/import/zendesk/ticket/comment/type_spec.rb new file mode 100644 index 000000000..2af45a32e --- /dev/null +++ b/spec/import/zendesk/ticket/comment/type_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/ticket/comment/local_id_lookup_backend_examples' + +RSpec.describe Import::Zendesk::Ticket::Comment::Type do + it_behaves_like 'local_id lookup backend' +end diff --git a/spec/import/zendesk/ticket/comment_factory_spec.rb b/spec/import/zendesk/ticket/comment_factory_spec.rb new file mode 100644 index 000000000..3148948be --- /dev/null +++ b/spec/import/zendesk/ticket/comment_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/ticket/sub_object_factory_examples' + +RSpec.describe Import::Zendesk::Ticket::CommentFactory do + it_behaves_like 'Import::Zendesk::Ticket::SubObjectFactory' +end diff --git a/spec/import/zendesk/ticket/comment_spec.rb b/spec/import/zendesk/ticket/comment_spec.rb new file mode 100644 index 000000000..8b49efe8a --- /dev/null +++ b/spec/import/zendesk/ticket/comment_spec.rb @@ -0,0 +1,637 @@ +require 'rails_helper' + +RSpec.describe Import::Zendesk::Ticket::Comment do + + context 'email' do + + it 'creates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'email', + source: double( + from: double(address: 'from@sender.tld'), + to: double(address: 'to@receiver.tld') + ), + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + create_structure = { + from: comment.via.source.from.address, + to: comment.via.source.to.address, + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 1, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(create_structure[:sender_id]) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id) + + expect(::Ticket::Article).to receive(:create).with(create_structure) + + described_class.new(comment, local_ticket, nil) + end + + it 'updates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'email', + source: double( + from: double(address: 'from@sender.tld'), + to: double(address: 'to@receiver.tld') + ), + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + update_structure = { + from: comment.via.source.from.address, + to: comment.via.source.to.address, + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 1, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(update_structure[:sender_id]) + + local_article = double() + expect(local_article).to receive(:update_attributes).with(update_structure) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id).and_return(local_article) + + described_class.new(comment, local_ticket, nil) + end + end + + context 'facebook' do + + context 'post' do + + it 'creates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'facebook', + source: double( + from: double(facebook_id: 3_129_033), + to: double(facebook_id: 1_230_920), + rel: 'post', + ), + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + create_structure = { + from: comment.via.source.from.facebook_id, + to: comment.via.source.to.facebook_id, + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 8, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(create_structure[:sender_id]) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id) + + expect(::Ticket::Article).to receive(:create).with(create_structure) + + described_class.new(comment, local_ticket, nil) + end + + it 'updates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'facebook', + source: double( + from: double(facebook_id: 3_129_033), + to: double(facebook_id: 1_230_920), + rel: 'post', + ), + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + update_structure = { + from: comment.via.source.from.facebook_id, + to: comment.via.source.to.facebook_id, + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 8, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(update_structure[:sender_id]) + + local_article = double() + expect(local_article).to receive(:update_attributes).with(update_structure) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id).and_return(local_article) + + described_class.new(comment, local_ticket, nil) + end + end + + context 'comment' do + + it 'creates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'facebook', + source: double( + from: double(facebook_id: 3_129_033), + to: double(facebook_id: 1_230_920), + rel: 'comment', + ), + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + create_structure = { + from: comment.via.source.from.facebook_id, + to: comment.via.source.to.facebook_id, + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 9, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(create_structure[:sender_id]) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id) + + expect(::Ticket::Article).to receive(:create).with(create_structure) + + described_class.new(comment, local_ticket, nil) + end + + it 'updates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'facebook', + source: double( + from: double(facebook_id: 3_129_033), + to: double(facebook_id: 1_230_920), + rel: 'comment', + ), + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + update_structure = { + from: comment.via.source.from.facebook_id, + to: comment.via.source.to.facebook_id, + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 9, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(update_structure[:sender_id]) + + local_article = double() + expect(local_article).to receive(:update_attributes).with(update_structure) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id).and_return(local_article) + + described_class.new(comment, local_ticket, nil) + end + end + end + + context 'twitter' do + + context 'mention' do + + it 'creates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'twitter', + source: double( + rel: 'mention', + ), + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + create_structure = { + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 6, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(create_structure[:sender_id]) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id) + + expect(::Ticket::Article).to receive(:create).with(create_structure) + + described_class.new(comment, local_ticket, nil) + end + + it 'updates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'twitter', + source: double( + rel: 'mention', + ), + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + update_structure = { + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 6, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(update_structure[:sender_id]) + + local_article = double() + expect(local_article).to receive(:update_attributes).with(update_structure) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id).and_return(local_article) + + described_class.new(comment, local_ticket, nil) + end + end + + context 'direct_message' do + + it 'creates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'twitter', + source: double( + rel: 'direct_message', + ), + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + create_structure = { + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 7, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(create_structure[:sender_id]) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id) + + expect(::Ticket::Article).to receive(:create).with(create_structure) + + described_class.new(comment, local_ticket, nil) + end + + it 'updates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'twitter', + source: double( + rel: 'direct_message', + ), + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + update_structure = { + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 7, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(update_structure[:sender_id]) + + local_article = double() + expect(local_article).to receive(:update_attributes).with(update_structure) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id).and_return(local_article) + + described_class.new(comment, local_ticket, nil) + end + end + end + + context 'web' do + + it 'creates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'web', + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + create_structure = { + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 11, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(create_structure[:sender_id]) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id) + + expect(::Ticket::Article).to receive(:create).with(create_structure) + + described_class.new(comment, local_ticket, nil) + end + + it 'updates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'web', + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + update_structure = { + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 11, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(update_structure[:sender_id]) + + local_article = double() + expect(local_article).to receive(:update_attributes).with(update_structure) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id).and_return(local_article) + + described_class.new(comment, local_ticket, nil) + end + end + + context 'sample_ticket' do + + it 'creates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'sample_ticket', + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + create_structure = { + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 10, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(create_structure[:sender_id]) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id) + + expect(::Ticket::Article).to receive(:create).with(create_structure) + + described_class.new(comment, local_ticket, nil) + end + + it 'updates' do + + comment = double( + id: 1337, + author_id: 42, + public: true, + html_body: '
Hello World!
', + via: double( + channel: 'sample_ticket', + ), + attachments: [] + ) + + local_ticket = double(id: 31_337) + + local_user_id = 99 + + update_structure = { + ticket_id: local_ticket.id, + body: comment.html_body, + content_type: 'text/html', + internal: !comment.public, + message_id: comment.id, + updated_by_id: local_user_id, + created_by_id: local_user_id, + sender_id: 23, + type_id: 10, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( comment.author_id ).and_return(local_user_id) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(update_structure[:sender_id]) + + local_article = double() + expect(local_article).to receive(:update_attributes).with(update_structure) + + expect(::Ticket::Article).to receive(:find_by).with(message_id: comment.id).and_return(local_article) + + described_class.new(comment, local_ticket, nil) + end + end + +end diff --git a/spec/import/zendesk/ticket/sub_object_factory_examples.rb b/spec/import/zendesk/ticket/sub_object_factory_examples.rb new file mode 100644 index 000000000..afd8502ac --- /dev/null +++ b/spec/import/zendesk/ticket/sub_object_factory_examples.rb @@ -0,0 +1,21 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.shared_examples 'Import::Zendesk::Ticket::SubObjectFactory' do + it_behaves_like 'Import::Factory' + + it 'tunnels local and remote ticket object to backend' do + + expect(described_class).to receive(:backend_class).and_return(Class) + expect(described_class).to receive('skip?') + expect(described_class).to receive(:pre_import_hook) + expect(described_class).to receive(:post_import_hook) + record = double() + local_ticket = double() + zendesk_ticket = double() + expect(Class).to receive(:new).with(record, local_ticket, zendesk_ticket) + parameter = double() + expect(parameter).to receive(:each).and_yield(record) + described_class.import(parameter, local_ticket, zendesk_ticket) + end +end diff --git a/spec/import/zendesk/ticket/tag_factory_spec.rb b/spec/import/zendesk/ticket/tag_factory_spec.rb new file mode 100644 index 000000000..02011c259 --- /dev/null +++ b/spec/import/zendesk/ticket/tag_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/ticket/sub_object_factory_examples' + +RSpec.describe Import::Zendesk::Ticket::TagFactory do + it_behaves_like 'Import::Zendesk::Ticket::SubObjectFactory' +end diff --git a/spec/import/zendesk/ticket/tag_spec.rb b/spec/import/zendesk/ticket/tag_spec.rb new file mode 100644 index 000000000..62bad3e2c --- /dev/null +++ b/spec/import/zendesk/ticket/tag_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +# required due to some of rails autoloading issues +require 'import/zendesk/ticket/tag' + +RSpec.describe Import::Zendesk::Ticket::Tag do + + it 'creates ticket tags' do + + tag = double(id: 'Test Tag') + + local_ticket = instance_double(::Ticket, id: 1337) + + zendesk_ticket = double(requester_id: 31_337) + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with(zendesk_ticket.requester_id) + expect(::Tag).to receive(:tag_add).with(hash_including(item: tag.id, o_id: local_ticket.id)) + + described_class.new(tag, local_ticket, zendesk_ticket) + end +end diff --git a/spec/import/zendesk/ticket_factory_spec.rb b/spec/import/zendesk/ticket_factory_spec.rb new file mode 100644 index 000000000..d92f06c4a --- /dev/null +++ b/spec/import/zendesk/ticket_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/base_factory_examples' + +RSpec.describe Import::Zendesk::TicketFactory do + it_behaves_like 'Import::Zendesk::BaseFactory' +end diff --git a/spec/import/zendesk/ticket_field_factory_spec.rb b/spec/import/zendesk/ticket_field_factory_spec.rb new file mode 100644 index 000000000..63d26ccf1 --- /dev/null +++ b/spec/import/zendesk/ticket_field_factory_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'import/zendesk/base_factory_examples' +require 'import/zendesk/local_id_mapper_hook_examples' + +RSpec.describe Import::Zendesk::TicketFieldFactory do + it_behaves_like 'Import::Zendesk::BaseFactory' + it_behaves_like 'Import::Zendesk::LocalIDMapperHook' +end diff --git a/spec/import/zendesk/ticket_field_spec.rb b/spec/import/zendesk/ticket_field_spec.rb new file mode 100644 index 000000000..508791f71 --- /dev/null +++ b/spec/import/zendesk/ticket_field_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/object_field_examples' + +RSpec.describe Import::Zendesk::TicketField do + it_behaves_like 'Import::Zendesk::ObjectField' +end diff --git a/spec/import/zendesk/ticket_spec.rb b/spec/import/zendesk/ticket_spec.rb new file mode 100644 index 000000000..94604708d --- /dev/null +++ b/spec/import/zendesk/ticket_spec.rb @@ -0,0 +1,123 @@ +require 'rails_helper' + +# required due to some of rails autoloading issues +require 'import/zendesk/ticket' + +RSpec.describe Import::Zendesk::Ticket do + + it 'creates' do + + ticket = double( + id: 1337, + subject: 'The ticket title', + description: 'An example ticket', + requester_id: 42, + group_id: 909, + organization_id: 101, + due_at: DateTime.tomorrow, + updated_at: DateTime.yesterday, + created_at: DateTime.yesterday, + tags: [], + comments: [], + custom_fields: [], + ) + + local_user_id = 23 + + expected_structure = { + id: ticket.id, + title: ticket.subject, + note: ticket.description, + group_id: 3, + customer_id: local_user_id, + organization_id: 89, + state: 13, + priority: 7, + pending_time: ticket.due_at, + updated_at: ticket.updated_at, + created_at: ticket.created_at, + updated_by_id: local_user_id, + created_by_id: local_user_id, + create_article_sender_id: 21, + create_article_type_id: 555, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( ticket.requester_id ).and_return(local_user_id) + + expect(Import::Zendesk::GroupFactory).to receive(:local_id).with( ticket.group_id ).and_return(expected_structure[:group_id]) + expect(Import::Zendesk::OrganizationFactory).to receive(:local_id).with( ticket.organization_id ).and_return(expected_structure[:organization_id]) + expect(Import::Zendesk::Priority).to receive(:lookup).with(ticket).and_return(expected_structure[:priority]) + expect(Import::Zendesk::State).to receive(:lookup).with(ticket).and_return(expected_structure[:state]) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(expected_structure[:create_article_sender_id]) + expect(Import::Zendesk::Ticket::Comment::Type).to receive(:local_id).with(ticket).and_return(expected_structure[:create_article_type_id]) + + local_ticket = double() + + expect(Import::Zendesk::Ticket::TagFactory).to receive(:import).with(ticket.tags, local_ticket, ticket) + expect(Import::Zendesk::Ticket::CommentFactory).to receive(:import).with(ticket.comments, local_ticket, ticket) + + expect(::Ticket).to receive(:find_by).with(id: expected_structure[:id]) + expect(::Ticket).to receive(:create).with(expected_structure).and_return(local_ticket) + + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + + created_instance = described_class.new(ticket) + end + + it 'updates' do + + ticket = double( + id: 1337, + subject: 'The ticket title', + description: 'An example ticket', + requester_id: 42, + group_id: 909, + organization_id: 101, + due_at: DateTime.tomorrow, + updated_at: DateTime.yesterday, + created_at: DateTime.yesterday, + tags: [], + comments: [], + custom_fields: [], + ) + + local_user_id = 23 + + expected_structure = { + id: ticket.id, + title: ticket.subject, + note: ticket.description, + group_id: 3, + customer_id: local_user_id, + organization_id: 89, + state: 13, + priority: 7, + pending_time: ticket.due_at, + updated_at: ticket.updated_at, + created_at: ticket.created_at, + updated_by_id: local_user_id, + created_by_id: local_user_id, + create_article_sender_id: 21, + create_article_type_id: 555, + } + + expect(Import::Zendesk::UserFactory).to receive(:local_id).with( ticket.requester_id ).and_return(local_user_id) + + expect(Import::Zendesk::GroupFactory).to receive(:local_id).with( ticket.group_id ).and_return(expected_structure[:group_id]) + expect(Import::Zendesk::OrganizationFactory).to receive(:local_id).with( ticket.organization_id ).and_return(expected_structure[:organization_id]) + expect(Import::Zendesk::Priority).to receive(:lookup).with(ticket).and_return(expected_structure[:priority]) + expect(Import::Zendesk::State).to receive(:lookup).with(ticket).and_return(expected_structure[:state]) + expect(Import::Zendesk::Ticket::Comment::Sender).to receive(:local_id).with(local_user_id).and_return(expected_structure[:create_article_sender_id]) + expect(Import::Zendesk::Ticket::Comment::Type).to receive(:local_id).with(ticket).and_return(expected_structure[:create_article_type_id]) + + local_ticket = double() + + expect(Import::Zendesk::Ticket::TagFactory).to receive(:import).with(ticket.tags, local_ticket, ticket) + expect(Import::Zendesk::Ticket::CommentFactory).to receive(:import).with(ticket.comments, local_ticket, ticket) + + expect(::Ticket).to receive(:find_by).with(id: expected_structure[:id]).and_return(local_ticket) + expect(local_ticket).to receive(:update_attributes).with(expected_structure) + + created_instance = described_class.new(ticket) + end +end diff --git a/spec/import/zendesk/user/group_spec.rb b/spec/import/zendesk/user/group_spec.rb new file mode 100644 index 000000000..d2100040f --- /dev/null +++ b/spec/import/zendesk/user/group_spec.rb @@ -0,0 +1,9 @@ +require 'rails_helper' +require 'import/zendesk/user/lookup_backend_examples' + +# required due to some of rails autoloading issues +require 'import/zendesk/user/group' + +RSpec.describe Import::Zendesk::User::Group do + it_behaves_like 'lookup backend' +end diff --git a/spec/import/zendesk/user/lookup_backend_examples.rb b/spec/import/zendesk/user/lookup_backend_examples.rb new file mode 100644 index 000000000..47ef2f94c --- /dev/null +++ b/spec/import/zendesk/user/lookup_backend_examples.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +RSpec.shared_examples 'lookup backend' do + it 'responds to for' do + expect(described_class).to respond_to(:for) + end +end diff --git a/spec/import/zendesk/user/role_spec.rb b/spec/import/zendesk/user/role_spec.rb new file mode 100644 index 000000000..5bf383d4f --- /dev/null +++ b/spec/import/zendesk/user/role_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' +require 'import/zendesk/user/lookup_backend_examples' + +# required due to some of rails autoloading issues +require 'import/zendesk/user/role' + +RSpec.describe Import::Zendesk::User::Role do + it_behaves_like 'lookup backend' + + it 'responds to map' do + expect(described_class).to respond_to(:map) + end +end diff --git a/spec/import/zendesk/user_factory_spec.rb b/spec/import/zendesk/user_factory_spec.rb new file mode 100644 index 000000000..e77608c89 --- /dev/null +++ b/spec/import/zendesk/user_factory_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'import/zendesk/base_factory_examples' +require 'import/zendesk/local_id_mapper_hook_examples' + +RSpec.describe Import::Zendesk::UserFactory do + it_behaves_like 'Import::Zendesk::BaseFactory' + it_behaves_like 'Import::Zendesk::LocalIDMapperHook' +end diff --git a/spec/import/zendesk/user_field_factory_spec.rb b/spec/import/zendesk/user_field_factory_spec.rb new file mode 100644 index 000000000..03dc38cec --- /dev/null +++ b/spec/import/zendesk/user_field_factory_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'import/zendesk/base_factory_examples' +require 'import/zendesk/local_id_mapper_hook_examples' + +RSpec.describe Import::Zendesk::UserFieldFactory do + it_behaves_like 'Import::Zendesk::BaseFactory' + it_behaves_like 'Import::Zendesk::LocalIDMapperHook' +end diff --git a/spec/import/zendesk/user_field_spec.rb b/spec/import/zendesk/user_field_spec.rb new file mode 100644 index 000000000..491c3af88 --- /dev/null +++ b/spec/import/zendesk/user_field_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/zendesk/object_field_examples' + +RSpec.describe Import::Zendesk::UserField do + it_behaves_like 'Import::Zendesk::ObjectField' +end diff --git a/spec/import/zendesk/user_spec.rb b/spec/import/zendesk/user_spec.rb new file mode 100644 index 000000000..3c3d43eaa --- /dev/null +++ b/spec/import/zendesk/user_spec.rb @@ -0,0 +1,165 @@ +require 'rails_helper' + +# required due to some of rails autoloading issues +require 'import/zendesk/user' + +RSpec.describe Import::Zendesk::User do + + it 'create_or_updates user' do + + user = double( + id: 1337, + name: 'Example User', + email: 'user@example.com', + phone: '+49-123-345673', + suspended: false, + notes: 'Nice guy', + verified: true, + organization_id: 42, + last_login_at: DateTime.yesterday, + photo: double(content_url: 'https://img.remote.tld/w40293402zz394eed'), + user_fields: [], + ) + + expected_structure = { + login: user.email, + firstname: user.name, + email: user.email, + phone: user.phone, + password: '', + active: !user.suspended, + groups: [1, 2, 3], + roles: [3], + note: user.notes, + verified: user.verified, + organization_id: 101, + last_login: user.last_login_at, + image_source: user.photo.content_url, + updated_by_id: 1, + created_by_id: 1 + } + + local_user = double(id: 31_337) + + expect(Import::Zendesk::User::Group).to receive(:for).with(user).and_return(expected_structure[:groups]) + expect(Import::Zendesk::User::Role).to receive(:for).with(user).and_return(expected_structure[:roles]) + expect(Import::Zendesk::OrganizationFactory).to receive(:local_id).with( user.organization_id ).and_return(expected_structure[:organization_id]) + + expect(::User).to receive(:create_or_update).with( expected_structure ).and_return(local_user) + + created_instance = described_class.new(user) + + expect(created_instance).to respond_to(:id) + expect(created_instance.id).to eq(local_user.id) + + expect(created_instance).to respond_to(:zendesk_id) + expect(created_instance.zendesk_id).to eq(user.id) + end + + it 'imports id as login if no email address is available' do + + user = double( + id: 1337, + name: 'Example User', + email: nil, + phone: '+49-123-345673', + suspended: false, + notes: 'Nice guy', + verified: true, + organization_id: 42, + last_login_at: DateTime.yesterday, + photo: double(content_url: 'https://img.remote.tld/w40293402zz394eed'), + user_fields: [], + ) + + expected_structure = { + login: user.id.to_s, + firstname: user.name, + email: user.email, + phone: user.phone, + password: '', + active: !user.suspended, + groups: [1, 2, 3], + roles: [3], + note: user.notes, + verified: user.verified, + organization_id: 101, + last_login: user.last_login_at, + image_source: user.photo.content_url, + updated_by_id: 1, + created_by_id: 1 + } + + local_user = double(id: 31_337) + + expect(Import::Zendesk::User::Group).to receive(:for).with(user).and_return(expected_structure[:groups]) + expect(Import::Zendesk::User::Role).to receive(:for).with(user).and_return(expected_structure[:roles]) + expect(Import::Zendesk::OrganizationFactory).to receive(:local_id).with( user.organization_id ).and_return(expected_structure[:organization_id]) + + expect(::User).to receive(:create_or_update).with( expected_structure ).and_return(local_user) + + created_instance = described_class.new(user) + + expect(created_instance).to respond_to(:id) + expect(created_instance.id).to eq(local_user.id) + + expect(created_instance).to respond_to(:zendesk_id) + expect(created_instance.zendesk_id).to eq(user.id) + end + + it 'handles import user credentials and privileges specially' do + + user = double( + id: 1337, + name: 'Example User', + email: 'user@example.com', + phone: '+49-123-345673', + suspended: false, + notes: 'Nice guy', + verified: true, + organization_id: 42, + last_login_at: DateTime.yesterday, + photo: double(content_url: 'https://img.remote.tld/w40293402zz394eed'), + user_fields: [], + ) + + password = 'apikeyprovidedfortheimportbytheuser' + + expected_structure = { + login: user.email, + firstname: user.name, + email: user.email, + phone: user.phone, + password: password, + active: !user.suspended, + groups: [1, 2, 3], + roles: [1, 2], + note: user.notes, + verified: user.verified, + organization_id: 101, + last_login: user.last_login_at, + image_source: user.photo.content_url, + updated_by_id: 1, + created_by_id: 1 + } + + local_user = double(id: 31_337) + + expect(Import::Zendesk::User::Group).to receive(:for).with(user).and_return(expected_structure[:groups]) + expect(Import::Zendesk::User::Role).to receive(:map).with(user, 'admin').and_return(expected_structure[:roles]) + expect(Import::Zendesk::OrganizationFactory).to receive(:local_id).with( user.organization_id ).and_return(expected_structure[:organization_id]) + + expect(Setting).to receive(:get).with('import_zendesk_endpoint_username').twice.and_return(user.email) + expect(Setting).to receive(:get).with('import_zendesk_endpoint_key').and_return(password) + + expect(::User).to receive(:create_or_update).with( expected_structure ).and_return(local_user) + + created_instance = described_class.new(user) + + expect(created_instance).to respond_to(:id) + expect(created_instance.id).to eq(local_user.id) + + expect(created_instance).to respond_to(:zendesk_id) + expect(created_instance.zendesk_id).to eq(user.id) + end +end diff --git a/spec/import/zendesk_spec.rb b/spec/import/zendesk_spec.rb index 7404bbe04..35ef0ea0f 100644 --- a/spec/import/zendesk_spec.rb +++ b/spec/import/zendesk_spec.rb @@ -1,6 +1,12 @@ require 'rails_helper' +require 'import/helper_examples' require 'import/importer_examples' +require 'import/async_examples' +require 'import/import_stats_examples' RSpec.describe Import::Zendesk do it_behaves_like 'Import backend' + it_behaves_like 'Import::Helper' + it_behaves_like 'Import::Async' + it_behaves_like 'Import::ImportStats' end diff --git a/test/integration/zendesk_import_browser_test.rb b/test/integration/zendesk_import_browser_test.rb index d2765a07b..156e8f362 100644 --- a/test/integration/zendesk_import_browser_test.rb +++ b/test/integration/zendesk_import_browser_test.rb @@ -99,7 +99,7 @@ class ZendeskImportBrowserTest < TestCase watch_for( css: '.js-ticket .js-done', value: '143', - timeout: 300, + timeout: 600, ) watch_for( diff --git a/test/integration/zendesk_import_test.rb b/test/integration/zendesk_import_test.rb index 8be3b7525..13be8dd44 100644 --- a/test/integration/zendesk_import_test.rb +++ b/test/integration/zendesk_import_test.rb @@ -72,7 +72,7 @@ class ZendeskImportTest < ActiveSupport::TestCase data: { firstname: 'Bob', lastname: 'Smith', - login: '1150734731', + login: 'bob.smith@znuny.com', email: 'bob.smith@znuny.com', active: true, phone: '00114124', @@ -86,7 +86,7 @@ class ZendeskImportTest < ActiveSupport::TestCase data: { firstname: 'Hansimerkur', lastname: '', - login: '1202726471', + login: 'hansimerkur@znuny.com', email: 'hansimerkur@znuny.com', active: true, lieblingstier: nil, @@ -99,7 +99,7 @@ class ZendeskImportTest < ActiveSupport::TestCase data: { firstname: 'Bernd', lastname: 'Hofbecker', - login: '1202726611', + login: 'bernd.hofbecker@znuny.com', email: 'bernd.hofbecker@znuny.com', active: true, }, @@ -111,7 +111,7 @@ class ZendeskImportTest < ActiveSupport::TestCase data: { firstname: 'Zendesk', lastname: '', - login: '1202737821', + login: 'noreply@zendesk.com', email: 'noreply@zendesk.com', active: true, }, @@ -123,7 +123,7 @@ class ZendeskImportTest < ActiveSupport::TestCase data: { firstname: 'Hans', lastname: 'Peter Wurst', - login: '1205512622', + login: 'hansimerkur+zd-c1@znuny.com', email: 'hansimerkur+zd-c1@znuny.com', active: true, },