diff --git a/.rspec b/.rspec new file mode 100644 index 000000000..34c5164d9 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/Gemfile b/Gemfile index 307af7e9b..39a5805e2 100644 --- a/Gemfile +++ b/Gemfile @@ -85,8 +85,10 @@ gem 'diffy' # in production environments by default. group :development, :test do + gem 'rspec-rails' gem 'test-unit' gem 'spring' + gem 'spring-commands-rspec' gem 'sqlite3' # code coverage diff --git a/Gemfile.lock b/Gemfile.lock index 81962222b..312357431 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,6 +77,7 @@ GEM delayed_job_active_record (4.1.1) activerecord (>= 3.0, < 5.1) delayed_job (>= 3.0, < 5) + diff-lcs (1.2.5) diffy (3.1.0) dnsruby (1.59.3) docile (1.1.5) @@ -271,6 +272,23 @@ GEM ffi (>= 0.5.0) ref (2.0.0) retriable (2.1.0) + rspec-core (3.5.4) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.5.0) + rspec-mocks (3.5.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.5.0) + rspec-rails (3.5.2) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-support (~> 3.5.0) + rspec-support (3.5.0) rubocop (0.42.0) parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) @@ -308,6 +326,8 @@ GEM slack-notifier (1.5.1) slop (3.6.0) spring (1.7.2) + spring-commands-rspec (1.0.4) + spring (>= 0.9.1) sprockets (3.7.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -403,6 +423,7 @@ DEPENDENCIES rails (= 4.2.7.1) rails-observers rb-fsevent + rspec-rails rubocop sass-rails selenium-webdriver @@ -411,6 +432,7 @@ DEPENDENCIES simplecov-rcov slack-notifier spring + spring-commands-rspec sprockets sqlite3 test-unit diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 000000000..d72fadf39 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require 'bundler/setup' +load Gem.bin_path('rspec-core', 'rspec') diff --git a/bin/spring b/bin/spring new file mode 100755 index 000000000..7fe232c3a --- /dev/null +++ b/bin/spring @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. + +unless defined?(Spring) + require 'rubygems' + require 'bundler' + + if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) + Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } + gem 'spring', match[1] + require 'spring/binstub' + end +end diff --git a/lib/import/base_factory.rb b/lib/import/base_factory.rb new file mode 100644 index 000000000..550d5ebd6 --- /dev/null +++ b/lib/import/base_factory.rb @@ -0,0 +1,28 @@ +module Import + module BaseFactory + + # rubocop:disable Style/ModuleFunction + extend self + + def import(_records) + raise 'Missing implementation for import method for this factory' + end + + def pre_import_hook(_records) + end + + def backend_class(_record) + "Import::#{module_name}".constantize + end + + def skip?(_record) + false + end + + private + + def module_name + name.to_s.sub(/Import::/, '').sub(/Factory/, '') + end + end +end diff --git a/lib/import/factory.rb b/lib/import/factory.rb new file mode 100644 index 000000000..baf6c35ed --- /dev/null +++ b/lib/import/factory.rb @@ -0,0 +1,16 @@ +module Import + module Factory + include Import::BaseFactory + + # 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 + end +end diff --git a/lib/import/helper.rb b/lib/import/helper.rb new file mode 100644 index 000000000..696f1c279 --- /dev/null +++ b/lib/import/helper.rb @@ -0,0 +1,31 @@ +module Import + module Helper + # rubocop:disable Style/ModuleFunction + extend self + + def check_import_mode + # check if system is in import mode + return true if Setting.get('import_mode') + raise 'System is not in import mode!' + end + + # log + def log(message) + thread_no = Thread.current[:thread_no] || '-' + Rails.logger.info "thread##{thread_no}: #{message}" + end + + # utf8 convert + def utf8_encode(data) + data.each { |key, value| + next if !value + next if value.class != String + data[key] = Encode.conv('utf8', value) + } + end + + def reset_primary_key_sequence(table) + DbHelper.import_post(table) + end + end +end diff --git a/lib/import/otrs.rb b/lib/import/otrs.rb index 0fb26b19e..cabf286e5 100644 --- a/lib/import/otrs.rb +++ b/lib/import/otrs.rb @@ -1,1609 +1,162 @@ -require 'base64' +# 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/otrs/ticket' +require 'import/otrs/ticket_factory' +require 'import/otrs/article_customer' +require 'import/otrs/article_customer_factory' +require 'import/otrs/article' +require 'import/otrs/article_factory' +require 'import/otrs/article/attachment_factory' +require 'import/otrs/history' +require 'import/otrs/history_factory' +require 'import/otrs/history/article' +require 'import/otrs/history/move' +require 'import/otrs/history/new_ticket' +require 'import/otrs/history/priority_update' +require 'import/otrs/history/state_update' +require 'store' +require 'store/object' +require 'store/provider/db' +require 'store/provider/file' module Import -end -module Import::OTRS + module OTRS + extend Import::Helper + extend Import::OTRS::ImportStats + extend Import::OTRS::Async + extend Import::OTRS::Diff -=begin + # rubocop:disable Style/ModuleFunction + extend self - result = request_json(Subaction: 'List', 1) + def start + log 'Start import...' - return + checks - { some json structure } + prerequisites - result = request_json(Subaction: 'List') + base_objects - return + updateable_objects - "some data string" + customer_user -=end + threaded_import('Ticket') - def self.request_json(data, data_only = false) - response = post(data) - if !response - raise "Can't connect to Zammad Migrator" - end - if !response.success? - raise "Can't connect to Zammad Migrator" - end - result = json(response) - if !result - raise 'Invalid response' - end - if data_only - result['Result'] - else - result - end - end - -=begin - - start get request to backend, add auth data automatically - - result = request('Subaction=List') - - return - - "some data string" - -=end - - def self.request(part) - url = Setting.get('import_otrs_endpoint') + part + ';Key=' + Setting.get('import_otrs_endpoint_key') - log 'GET: ' + url - response = UserAgent.get( - url, - {}, - { - open_timeout: 10, - read_timeout: 60, - total_timeout: 180, - user: Setting.get('import_otrs_user'), - password: Setting.get('import_otrs_password'), - }, - ) - if !response.success? - log "ERROR: #{response.error}" - return - end - response - end - -=begin - - start post request to backend, add auth data automatically - - result = request('Subaction=List') - - return - - "some data string" - -=end - - def self.post(data, url = nil) - if !url - url = Setting.get('import_otrs_endpoint') - data['Action'] = 'ZammadMigrator' - end - data['Key'] = Setting.get('import_otrs_endpoint_key') - log 'POST: ' + url - log 'PARAMS: ' + data.inspect - open_timeout = 10 - read_timeout = 120 - total_timeout = 360 - if data.empty? - open_timeout = 6 - read_timeout = 20 - total_timeout = 120 - end - response = UserAgent.post( - url, - data, - { - open_timeout: open_timeout, - read_timeout: read_timeout, - total_timeout: total_timeout, - user: Setting.get('import_otrs_user'), - password: Setting.get('import_otrs_password'), - }, - ) - if !response.success? - log "ERROR: #{response.error}" - return - end - response - end - -=begin - - start post request to backend, add auth data automatically - - result = json('some response string') - - return - - {} - -=end - - def self.json(response) - data = Encode.conv('utf8', response.body.to_s) - JSON.parse(data) - end - -=begin - - start auth on OTRS - just for experimental reasons - - result = auth(username, password) - - return - - { ..user structure.. } - -=end - - def self.auth(username, password) - url = Setting.get('import_otrs_endpoint') - url.gsub!('ZammadMigrator', 'ZammadSSO') - response = post( { Action: 'ZammadSSO', Subaction: 'Auth', User: username, Pw: password }, url ) - return if !response - return if !response.success? - - result = json(response) - result - end - -=begin - - request session data - just for experimental reasons - - result = session(session_id) - - return - - { ..session structure.. } - -=end - - def self.session(session_id) - url = Setting.get('import_otrs_endpoint') - url.gsub!('ZammadMigrator', 'ZammadSSO') - response = post( { Action: 'ZammadSSO', Subaction: 'SessionCheck', SessionID: session_id }, url ) - return if !response - return if !response.success? - result = json(response) - result - end - -=begin - - load objects from otrs - - result = load('SysConfig') - - return - - [ - { ..object1.. }, - { ..object2.. }, - { ..object3.. }, - ] - -=end - - def self.load( object, limit = '', offset = '', diff = 0 ) - request_json( { Subaction: 'Export', Object: object, Limit: limit, Offset: offset, Diff: diff }, 1 ) - end - -=begin - - start get request to backend to check connection - - result = connection_test - - return - - true | false - -=end - - def self.connection_test - request_json({}) - end - -=begin - - get object statistic from remote server ans save it in cache - - result = statistic('Subaction=List') - - return - - { - 'Ticket' => 1234, - 'User' => 123, - 'SomeObject' => 999, - } - -=end - - def self.statistic - - # check cache - cache = Cache.get('import_otrs_stats') - if cache - return cache + true end - # retrive statistic - statistic = request_json( { Subaction: 'List' }, 1) - if statistic - Cache.write('import_otrs_stats', statistic) - end - statistic - end + private -=begin - - return current import state - - result = current_state - - return - - { - Ticket: { - total: 1234, - done: 13, - }, - Base: { - total: 1234, - done: 13, - }, - } - -=end - - def self.current_state - data = statistic - base = Group.count + Ticket::State.count + Ticket::Priority.count - base_total = data['Queue'] + data['State'] + data['Priority'] - user = User.count - user_total = data['User'] + data['CustomerUser'] - data = { - Base: { - done: base, - total: base_total || 0, - }, - User: { - done: user, - total: user_total || 0, - }, - Ticket: { - done: Ticket.count, - total: data['Ticket'] || 0, - }, - } - data - end - - # - # start import - # - # Import::OTRS.start - # - - def self.start - log 'Start import...' - - # check if system is in import mode - if !Setting.get('import_mode') - raise 'System is not in import mode!' + def checks + check_import_mode + Import::OTRS::Requester.connection_test end - result = request_json({}) - if !result['Success'] - raise 'API key not valid!' + def prerequisites + # make sure to create store type otherwise + # it might lead to race conditions while + # creating it in different import threads + Store::Object.create_if_not_exists(name: 'Ticket::Article') end - # set settings - settings = load('SysConfig') - setting(settings) - - # dynamic fields - dynamic_fields = load('DynamicField') - object_manager(dynamic_fields) - - # email accounts - #accounts = load('PostMasterAccount') - #account(accounts) - - # email filter - #filters = load('PostMasterFilter') - #filter(filters) - - # create states - states = load('State') - ActiveRecord::Base.transaction do - state(states) + def import(remote_object) + log "loading #{remote_object}..." + import_action(remote_object) end - # create priorities - priorities = load('Priority') - ActiveRecord::Base.transaction do - priority(priorities) - end + def threaded_import(remote_object, args = {}) + thread_count = args[:threads] || 8 + limit = args[:limit] || 20 - # create groups - queues = load('Queue') - ActiveRecord::Base.transaction do - ticket_group(queues) - end + Thread.abort_on_exception = true + threads = {} + (1..thread_count).each { |thread| - # get agents groups - groups = load('Group') + threads[thread] = Thread.new { - # get agents roles - roles = load('Role') + Thread.current[:thread_no] = thread + Thread.current[:loop_count] = 0 - # create agents - users = load('User') - ActiveRecord::Base.transaction do - user(users, groups, roles, queues) - end + log "Importing #{remote_object} in steps of #{limit}" + loop do + # get the offset for the current thread and loop count + thread_offset_base = (Thread.current[:thread_no] - 1) * limit + thread_step = thread_count * limit + offset = Thread.current[:loop_count] * thread_step + thread_offset_base - # create organizations - organizations = load('Customer') - ActiveRecord::Base.transaction do - organization(organizations) - end + break if !imported?( + remote_object: remote_object, + limit: limit, + offset: offset, + diff: args[:diff] + ) - # create customers - count = 0 - steps = 50 - run = true - while run - count += steps - records = load('CustomerUser', steps, count - steps) - if !records || !records[0] - log 'all customers imported.' - run = false - next - end - customer(records, organizations) - end - - Thread.abort_on_exception = true - thread_count = 8 - threads = {} - steps = 20 - (1..thread_count).each { |thread| - - threads[thread] = Thread.new { - - log "Started import thread# #{thread} ..." - Thread.current[:thread_no] = thread - Thread.current[:loop_count] = 0 - - loop do - # get the offset for the current thread and loop count - thread_offset_base = (Thread.current[:thread_no] - 1) * steps - thread_step = thread_count * steps - offset = Thread.current[:loop_count] * thread_step + thread_offset_base - - log "loading... thread# #{thread} ..." - records = load( 'Ticket', steps, offset) - if !records || !records[0] - log "... thread# #{thread}, no more work." - break + Thread.current[:loop_count] += 1 end - _ticket_result(records, thread) - - Thread.current[:loop_count] += 1 - end - ActiveRecord::Base.connection.close + ActiveRecord::Base.connection.close + } } - } - (1..thread_count).each { |thread| - threads[thread].join - } + (1..thread_count).each { |thread| + threads[thread].join + } + end - true - end - -=begin - start import in background - - Import::OTRS.start_bg -=end - - def self.start_bg - Setting.reload - - Import::OTRS.connection_test - - # start thread to observe current state - status_update_thread = Thread.new { + def limit_import(remote_object, args = {}) + offset = 0 + limit = args[:limit] || 20 + log "Importing #{remote_object} in steps of #{limit}" 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::OTRS.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::OTRS.status_bg - -=end - - def self.status_bg - state = Cache.get('import:state') - return state if state - { - message: 'not running', - } - end - - def self.diff_worker - return if !Setting.get('import_mode') - return if Setting.get('import_otrs_endpoint') == 'http://otrs_host/otrs' - diff - end - - def self.diff - log 'Start diff...' - - # check if system is in import mode - if !Setting.get('import_mode') - raise 'System is not in import mode!' - end - - # create states - states = load('State') - state(states) - - # create priorities - priorities = load('Priority') - priority(priorities) - - # create groups - queues = load('Queue') - ticket_group(queues) - - # get agents groups - groups = load('Group') - - # get agents roles - roles = load('Role') - - # create agents - users = load('User') - user(users, groups, roles, queues) - - # create organizations - organizations = load('Customer') - organization(organizations) - - # get changed tickets - ticket_diff - - end - - def self.ticket_diff - count = 0 - run = true - steps = 20 - while run - count += steps - log 'loading... diff ...' - records = load( 'Ticket', steps, count - steps, 1 ) - if !records || !records[0] - log '... no more work.' - run = false - next - end - _ticket_result(records) - end - - end - - def self._ticket_result(result, _thread = '-') - map = { - Ticket: { - Changed: :updated_at, - Created: :created_at, - CreateBy: :created_by_id, - TicketNumber: :number, - QueueID: :group_id, - StateID: :state_id, - PriorityID: :priority_id, - Owner: :owner, - CustomerUserID: :customer, - Title: :title, - TicketID: :id, - FirstResponse: :first_response_at, - #FirstResponseTimeDestinationDate: :first_response_escalation_at, - #FirstResponseInMin: :first_response_in_min, - #FirstResponseDiffInMin: :first_response_diff_in_min, - Closed: :close_at, - #SoltutionTimeDestinationDate: :close_escalation_at, - #CloseTimeInMin: :close_in_min, - #CloseTimeDiffInMin: :close_diff_in_min, - }, - Article: { - SenderType: :sender, - ArticleType: :type, - TicketID: :ticket_id, - ArticleID: :id, - Body: :body, - From: :from, - To: :to, - Cc: :cc, - Subject: :subject, - InReplyTo: :in_reply_to, - MessageID: :message_id, - #ReplyTo: :reply_to, - References: :references, - Changed: :updated_at, - Created: :created_at, - ChangedBy: :updated_by_id, - CreatedBy: :created_by_id, - }, - } - - result.each { |record| - - # cleanup values - _cleanup(record) - - _utf8_encode(record) - - ticket_new = { - title: '', - created_by_id: 1, - updated_by_id: 1, - } - map[:Ticket].each { |key, value| - next if !record.key?(key.to_s) - ticket_new[value] = record[key.to_s] - } - - record.keys.each { |key| - - key_string = key.to_s - - next if !key_string.start_with?('DynamicField_') - dynamic_field_name = key_string[13, key_string.length] - - next if skip_fields.include?( dynamic_field_name ) - dynamic_field_name = convert_df_name(dynamic_field_name) - - ticket_new[dynamic_field_name.to_sym] = record[key_string] - } - - # find owner - if ticket_new[:owner] - user = User.find_by(login: ticket_new[:owner].downcase) - ticket_new[:owner_id] = if user - user.id - else - 1 - end - ticket_new.delete(:owner) - end - - record['Articles'].each { |article| - # utf8 encode - _utf8_encode(article) - # lookup customers to create first - _article_based_customers(article) - } - - # find customer - if ticket_new[:customer] - user = User.lookup(login: ticket_new[:customer].downcase) - ticket_new[:customer_id] = if user - user.id - else - _first_customer_id(record['Articles']) - end - ticket_new.delete(:customer) - else - ticket_new[:customer_id] = 1 - end - - # update or create ticket - ticket_old = Ticket.find_by(id: ticket_new[:id]) - if ticket_old - log "update Ticket.find(#{ticket_new[:id]})" - ticket_old.update_attributes(ticket_new) - else - log "add Ticket.find(#{ticket_new[:id]})" - - begin - ticket = Ticket.new(ticket_new) - ticket.id = ticket_new[:id] - ticket.save - _reset_pk('tickets') - rescue ActiveRecord::RecordNotUnique - log "Ticket #{ticket_new[:id]} is handled by another thead, skipping." - next - end - end - - record['Articles'].each do |article| - - retries = 3 - begin - - ActiveRecord::Base.transaction do - - # get article values - article_new = { - created_by_id: 1, - updated_by_id: 1, - } - - map[:Article].each { |key, value| - next if !article.key?(key.to_s) - article_new[value] = article[key.to_s] - } - - if article_new[:sender] == 'customer' - article_new[:sender_id] = Ticket::Article::Sender.lookup(name: 'Customer').id - article_new.delete(:sender) - end - if article_new[:sender] == 'agent' - article_new[:sender_id] = Ticket::Article::Sender.lookup(name: 'Agent').id - article_new.delete(:sender) - end - if article_new[:sender] == 'system' - article_new[:sender_id] = Ticket::Article::Sender.lookup(name: 'System').id - article_new.delete(:sender) - end - - if article_new[:type] == 'email-external' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'email').id - article_new[:internal] = false - elsif article_new[:type] == 'email-internal' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'email').id - article_new[:internal] = true - elsif article_new[:type] == 'note-external' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'note').id - article_new[:internal] = false - elsif article_new[:type] == 'note-internal' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'note').id - article_new[:internal] = true - elsif article_new[:type] == 'phone' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'phone').id - article_new[:internal] = false - elsif article_new[:type] == 'webrequest' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'web').id - article_new[:internal] = false - else - article_new[:type_id] = 9 - end - article_new.delete(:type) - article_object = Ticket::Article.find_by(id: article_new[:id]) - - # set state types - if article_object - log "update Ticket::Article.find(#{article_new[:id]})" - article_object.update_attributes(article_new) - else - log "add Ticket::Article.find(#{article_new[:id]})" - begin - article_object = Ticket::Article.new(article_new) - article_object.id = article_new[:id] - article_object.save - _reset_pk('ticket_articles') - rescue ActiveRecord::RecordNotUnique - log "Ticket #{ticket_new[:id]} (article #{article_new[:id]}) is handled by another thead, skipping." - next - end - end - - next if !article['Attachments'] - next if article['Attachments'].empty? - - # TODO: refactor - # check if there are attachments present - if !article_object.attachments.empty? - - # skip attachments if count is equal - next if article_object.attachments.count == article['Attachments'].count - - # if the count differs delete all so we - # can have a fresh start - article_object.attachments.each(&:delete) - end - - # import article attachments - article['Attachments'].each { |attachment| - - filename = Base64.decode64(attachment['Filename']) - - Store.add( - object: 'Ticket::Article', - o_id: article_object.id, - filename: filename, - data: Base64.decode64(attachment['Content']), - preferences: { - 'Mime-Type' => attachment['ContentType'], - 'Content-ID' => attachment['ContentID'], - 'content-alternative' => attachment['ContentAlternative'], - }, - created_by_id: 1, - ) - } - end - rescue ActiveRecord::RecordNotUnique => e - log "Ticket #{ticket_new[:id]} - RecordNotUnique: #{e}" - sleep rand 3 - retry if !(retries -= 1).zero? - raise - end - end - - #puts "HS: #{record['History'].inspect}" - record['History'].each { |history| - - begin - if history['HistoryType'] == 'NewTicket' - History.add( - id: history['HistoryID'], - o_id: history['TicketID'], - history_type: 'created', - history_object: 'Ticket', - created_at: history['CreateTime'], - created_by_id: history['CreateBy'] - ) - elsif history['HistoryType'] == 'StateUpdate' - data = history['Name'] - # "%%new%%open%%" - from = nil - to = nil - if data =~ /%%(.+?)%%(.+?)%%/ - from = $1 - to = $2 - state_from = Ticket::State.lookup(name: from) - state_to = Ticket::State.lookup(name: to) - if state_from - from_id = state_from.id - end - if state_to - to_id = state_to.id - end - end - History.add( - id: history['HistoryID'], - o_id: history['TicketID'], - history_type: 'updated', - history_object: 'Ticket', - history_attribute: 'state', - value_from: from, - id_from: from_id, - value_to: to, - id_to: to_id, - created_at: history['CreateTime'], - created_by_id: history['CreateBy'] - ) - elsif history['HistoryType'] == 'Move' - data = history['Name'] - # "%%Queue1%%5%%Postmaster%%1" - from = nil - to = nil - if data =~ /%%(.+?)%%(.+?)%%(.+?)%%(.+?)$/ - from = $1 - from_id = $2 - to = $3 - to_id = $4 - end - History.add( - id: history['HistoryID'], - o_id: history['TicketID'], - history_type: 'updated', - history_object: 'Ticket', - history_attribute: 'group', - value_from: from, - value_to: to, - id_from: from_id, - id_to: to_id, - created_at: history['CreateTime'], - created_by_id: history['CreateBy'] - ) - elsif history['HistoryType'] == 'PriorityUpdate' - data = history['Name'] - # "%%3 normal%%3%%5 very high%%5" - from = nil - to = nil - if data =~ /%%(.+?)%%(.+?)%%(.+?)%%(.+?)$/ - from = $1 - from_id = $2 - to = $3 - to_id = $4 - end - History.add( - id: history['HistoryID'], - o_id: history['TicketID'], - history_type: 'updated', - history_object: 'Ticket', - history_attribute: 'priority', - value_from: from, - value_to: to, - id_from: from_id, - id_to: to_id, - created_at: history['CreateTime'], - created_by_id: history['CreateBy'] - ) - elsif history['ArticleID'] && !history['ArticleID'].to_i.zero? - History.add( - id: history['HistoryID'], - o_id: history['ArticleID'], - history_type: 'created', - history_object: 'Ticket::Article', - related_o_id: history['TicketID'], - related_history_object: 'Ticket', - created_at: history['CreateTime'], - created_by_id: history['CreateBy'] - ) - end - - rescue ActiveRecord::RecordNotUnique - log "Ticket #{ticket_new[:id]} (history #{history['HistoryID']}) is handled by another thead, skipping." - next - end - } - } - end - - # sync ticket states - def self.state(records) - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - Name: :name, - ID: :id, - ValidID: :active, - Comment: :note, - } - - # rename states to handle not uniq issues - Ticket::State.all.each { |state| - state.name = state.name + '_tmp' - state.save - } - - records.each { |state| - _set_valid(state) - - # get new attributes - state_new = { - created_by_id: 1, - updated_by_id: 1, - } - map.each { |key, value| - next if !state.key?(key.to_s) - state_new[value] = state[key.to_s] - } - - # check if state already exists - state_old = Ticket::State.lookup(id: state_new[:id]) - - # set state types - if state['TypeName'] == 'pending auto' - state['TypeName'] = 'pending action' - end - state_type = Ticket::StateType.lookup(name: state['TypeName']) - state_new[:state_type_id] = state_type.id - if state_old - state_old.update_attributes(state_new) - else - state = Ticket::State.new(state_new) - state.id = state_new[:id] - state.save - _reset_pk('ticket_states') - end - } - end - - # sync ticket priorities - def self.priority(records) - - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - Name: :name, - ID: :id, - ValidID: :active, - Comment: :note, - } - - records.each { |priority| - _set_valid(priority) - - # get new attributes - priority_new = { - created_by_id: 1, - updated_by_id: 1, - } - map.each { |key, value| - next if !priority.key?(key.to_s) - priority_new[value] = priority[key.to_s] - } - - # check if state already exists - priority_old = Ticket::Priority.lookup(id: priority_new[:id]) - - # set state types - if priority_old - priority_old.update_attributes(priority_new) - else - priority = Ticket::Priority.new(priority_new) - priority.id = priority_new[:id] - priority.save - _reset_pk('ticket_priorities') - end - } - end - - # sync ticket groups / queues - def self.ticket_group(records) - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - Name: :name, - QueueID: :id, - ValidID: :active, - Comment: :note, - } - - records.each { |group| - _set_valid(group) - - # get new attributes - group_new = { - created_by_id: 1, - updated_by_id: 1, - } - map.each { |key, value| - next if !group.key?(key.to_s) - group_new[value] = group[key.to_s] - } - - # check if state already exists - group_old = Group.lookup(id: group_new[:id]) - - # set state types - if group_old - group_old.update_attributes(group_new) - else - group = Group.new(group_new) - group.id = group_new[:id] - group.save - _reset_pk('groups') - end - } - end - - # sync agents - def self.user(records, groups, roles, queues) - - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - UserID: :id, - ValidID: :active, - Comment: :note, - UserEmail: :email, - UserFirstname: :firstname, - UserLastname: :lastname, - UserLogin: :login, - UserPw: :password, - } - - records.each { |user| - _set_valid(user) - - # get roles - role_ids = get_roles_ids(user, groups, roles, queues) - - # get groups - group_ids = get_queue_ids(user, groups, roles, queues) - - # get new attributes - user_new = { - created_by_id: 1, - updated_by_id: 1, - source: 'OTRS Import', - role_ids: role_ids, - group_ids: group_ids, - } - map.each { |key, value| - next if !user.key?(key.to_s) - user_new[value] = user[key.to_s] - } - - # set pw - if user_new[:password] - user_new[:password] = "{sha2}#{user_new[:password]}" - end - - # check if agent already exists - user_old = User.lookup(id: user_new[:id]) - - # check if login is already used - login_in_use = User.where( "login = ? AND id != #{user_new[:id]}", user_new[:login].downcase ).count - if login_in_use.positive? - user_new[:login] = "#{user_new[:login]}_#{user_new[:id]}" - end - - # create / update agent - if user_old - log "update User.find(#{user_old[:id]})" - - # only update roles if different (reduce sql statements) - if user_old.role_ids == user_new[:role_ids] - user_new.delete(:role_ids) - end - - user_old.update_attributes(user_new) - else - log "add User.find(#{user_new[:id]})" - user = User.new(user_new) - user.id = user_new[:id] - user.save - _reset_pk('users') - end - } - end - - def self.get_queue_ids(user, _groups, _roles, queues) - queue_ids = [] - - # lookup by groups - user['GroupIDs'].each { |group_id, permissions| - queues.each { |queue_lookup| - - next if queue_lookup['GroupID'] != group_id - next if !permissions - next if !permissions.include?('rw') - - queue_ids.push queue_lookup['QueueID'] - } - } - - # lookup by roles - - # roles of user - # groups of roles - # queues of group - - queue_ids - end - - def self.get_roles_ids(user, groups, roles, _queues) - local_roles = ['Agent'] - local_role_ids = [] - - # apply group permissions - user['GroupIDs'].each { |group_id, permissions| - groups.each { |group_lookup| - - next if group_id != group_lookup['ID'] - next if !permissions - - if group_lookup['Name'] == 'admin' && permissions.include?('rw') - local_roles.push 'Admin' - end - - next if group_lookup['Name'] !~ /^(stats|report)/ - next if !( permissions.include?('ro') || permissions.include?('rw') ) - - local_roles.push 'Report' - } - } - - # apply role permissions - user['RoleIDs'].each { |role_id| - - # get groups of role - roles.each { |role| - next if role['ID'] != role_id - - # verify group names - role['GroupIDs'].each { |group_id, permissions| - groups.each { |group_lookup| - - next if group_id != group_lookup['ID'] - next if !permissions - - if group_lookup['Name'] == 'admin' && permissions.include?('rw') - local_roles.push 'Admin' - end - - next if group_lookup['Name'] !~ /^(stats|report)/ - next if !( permissions.include?('ro') || permissions.include?('rw') ) - - local_roles.push 'Report' - } - } - } - } - - local_roles.each { |role| - role_lookup = Role.lookup(name: role) - next if !role_lookup - local_role_ids.push role_lookup.id - } - local_role_ids - end - - # sync customers - - def self.customer(records, organizations) - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - ValidID: :active, - UserComment: :note, - UserEmail: :email, - UserFirstname: :firstname, - UserLastname: :lastname, - UserLogin: :login, - UserPassword: :password, - UserPhone: :phone, - UserFax: :fax, - UserMobile: :mobile, - UserStreet: :street, - UserZip: :zip, - UserCity: :city, - UserCountry: :country, - } - - role_agent = Role.lookup(name: 'Agent') - role_customer = Role.lookup(name: 'Customer') - - records.each { |user| - _set_valid(user) - - # get new attributes - user_new = { - created_by_id: 1, - updated_by_id: 1, - source: 'OTRS Import', - organization_id: get_organization_id(user, organizations), - role_ids: [ role_customer.id ], - } - map.each { |key, value| - next if !user.key?(key.to_s) - user_new[value] = user[key.to_s] - } - - # check if customer already exists - user_old = User.lookup(login: user_new[:login]) - - # create / update agent - if user_old - - # do not update user if it is already agent - if !user_old.role_ids.include?(role_agent.id) - - # only update roles if different (reduce sql statements) - if user_old.role_ids == user_new[:role_ids] - user_new.delete(:role_ids) - end - log "update User.find(#{user_old[:id]})" - user_old.update_attributes(user_new) - end - else - log "add User.find(#{user_new[:id]})" - user = User.new(user_new) - user.save - _reset_pk('users') - end - } - end - - def self.get_organization_id(user, organizations) - organization_id = nil - if user['UserCustomerID'] - organizations.each { |organization| - next if user['UserCustomerID'] != organization['CustomerID'] - organization = Organization.lookup(name: organization['CustomerCompanyName']) - organization_id = organization.id - } - end - organization_id - end - - # sync organizations - def self.organization(records) - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - CustomerCompanyName: :name, - ValidID: :active, - CustomerCompanyComment: :note, - } - - records.each { |organization| - _set_valid(organization) - - # get new attributes - organization_new = { - created_by_id: 1, - updated_by_id: 1, - } - map.each { |key, value| - next if !organization.key?(key.to_s) - organization_new[value] = organization[key.to_s] - } - - # check if state already exists - organization_old = Organization.lookup(name: organization_new[:name]) - - # set state types - if organization_old - organization_old.update_attributes(organization_new) - else - organization = Organization.new(organization_new) - organization.id = organization_new[:id] - organization.save - _reset_pk('organizations') - end - } - end - - # sync settings - def self.setting(records) - - records.each { |setting| - - # fqdn - if setting['Key'] == 'FQDN' - Setting.set('fqdn', setting['Value']) - end - - # http type - if setting['Key'] == 'HttpType' - Setting.set('http_type', setting['Value']) - end - - # system id - if setting['Key'] == 'SystemID' - Setting.set('system_id', setting['Value']) - end - - # organization - if setting['Key'] == 'Organization' - Setting.set('organization', setting['Value']) - end - - # sending emails - if setting['Key'] == 'SendmailModule' - # TODO - end - - # number generater - if setting['Key'] == 'Ticket::NumberGenerator' - if setting['Value'] == 'Kernel::System::Ticket::Number::DateChecksum' - Setting.set('ticket_number', 'Ticket::Number::Date') - Setting.set('ticket_number_date', { checksum: true }) - elsif setting['Value'] == 'Kernel::System::Ticket::Number::Date' - Setting.set('ticket_number', 'Ticket::Number::Date') - Setting.set('ticket_number_date', { checksum: false }) - end - end - - # ticket hook - if setting['Key'] == 'Ticket::Hook' - Setting.set('ticket_hook', setting['Value']) - end - } - end - - # dynamic fields - def self.object_manager(dynamic_fields) - - dynamic_fields.each { |dynamic_field| - - if dynamic_field['ObjectType'] != 'Ticket' - log "ERROR: Unsupported dynamic field object type '#{dynamic_field['ObjectType']}' for dynamic field '#{dynamic_field['Name']}'" - next - end - - next if skip_fields.include?( dynamic_field['Name'] ) - - internal_name = convert_df_name(dynamic_field['Name']) - - attribute = ObjectManager::Attribute.get( - object: dynamic_field['ObjectType'], - name: internal_name, - ) - next if !attribute.nil? - - object_manager_config = { - object: dynamic_field['ObjectType'], - name: internal_name, - display: dynamic_field['Label'], - screens: { - view: { - '-all-' => { - shown: true, - }, - }, - }, - active: true, - editable: dynamic_field['InternalField'] == '0', - position: dynamic_field['FieldOrder'], - created_by_id: 1, - updated_by_id: 1, - } - - if dynamic_field['FieldType'] == 'Text' - - object_manager_config[:data_type] = 'input' - object_manager_config[:data_option] = { - default: dynamic_field['Config']['DefaultValue'], - type: 'text', - maxlength: 255, - null: false, - } - elsif dynamic_field['FieldType'] == 'TextArea' - - object_manager_config[:data_type] = 'textarea' - object_manager_config[:data_option] = { - default: dynamic_field['Config']['DefaultValue'], - rows: dynamic_field['Config']['Rows'], - null: false, - } - elsif dynamic_field['FieldType'] == 'Checkbox' - - object_manager_config[:data_type] = 'boolean' - object_manager_config[:data_option] = { - default: dynamic_field['Config']['DefaultValue'] == '1', - options: { - true => 'Yes', - false => 'No', - }, - null: false, - translate: true, - } - elsif dynamic_field['FieldType'] == 'DateTime' - - object_manager_config[:data_type] = 'datetime' - object_manager_config[:data_option] = { - future: dynamic_field['Config']['YearsInFuture'] != '0', - past: dynamic_field['Config']['YearsInPast'] != '0', - diff: dynamic_field['Config']['DefaultValue'].to_i / 60 / 60, - null: false, - } - elsif dynamic_field['FieldType'] == 'Date' - - object_manager_config[:data_type] = 'date' - object_manager_config[:data_option] = { - future: dynamic_field['Config']['YearsInFuture'] != '0', - past: dynamic_field['Config']['YearsInPast'] != '0', - diff: dynamic_field['Config']['DefaultValue'].to_i / 60 / 60 / 24, - null: false, - } - elsif dynamic_field['FieldType'] == 'Dropdown' - - object_manager_config[:data_type] = 'select' - object_manager_config[:data_option] = { - default: '', - multiple: false, - options: dynamic_field['Config']['PossibleValues'], - null: dynamic_field['Config']['PossibleNone'] == '1', - translate: dynamic_field['Config']['TranslatableValues'] == '1', - } - elsif dynamic_field['FieldType'] == 'Multiselect' - - object_manager_config[:data_type] = 'select' - object_manager_config[:data_option] = { - default: '', - multiple: true, - options: dynamic_field['Config']['PossibleValues'], - null: dynamic_field['Config']['PossibleNone'] == '1', - translate: dynamic_field['Config']['TranslatableValues'] == '1', - } - else - log "ERROR: Unsupported dynamic field field type '#{dynamic_field['FieldType']}' for dynamic field '#{dynamic_field['Name']}'" - next - end - - ObjectManager::Attribute.add( object_manager_config ) - ObjectManager::Attribute.migration_execute(false) - } - - end - - def self.convert_df_name(dynamic_field_name) - new_name = dynamic_field_name.underscore - new_name.sub(/\_id(s)?\z/, "_no#{$1}") - end - - # log - def self.log(message) - thread_no = Thread.current[:thread_no] || '-' - Rails.logger.info "thread##{thread_no}: #{message}" - end - - # set translate valid ids to active = true|false - def self._set_valid(record) - - # map - record['ValidID'] = if record['ValidID'].to_s == '3' - false - elsif record['ValidID'].to_s == '2' - false - elsif record['ValidID'].to_s == '1' - true - elsif record['ValidID'].to_s == '0' - false - - # fallback - else - true - end - end - - # cleanup invalid values - def self._cleanup(record) - record.each { |key, value| - if value == '0000-00-00 00:00:00' - record[key] = nil - end - } - - # fix OTRS 3.1 bug, no close time if ticket is created - if record['StateType'] == 'closed' && (!record['Closed'] || record['Closed'].empty?) - record['Closed'] = record['Created'] - end - end - - # utf8 convert - def self._utf8_encode(data) - data.each { |key, value| - next if !value - next if value.class != String - data[key] = Encode.conv('utf8', value) - } - end - - # reset primary key sequences - def self._reset_pk(table) - DbHelper.import_post(table) - end - - # create customers for article - def self._article_based_customers(article) - - # create customer/sender if needed - return if article['sender'] != 'customer' - return if article['created_by_id'].to_i != 1 - return if article['from'].empty? - - email = nil - begin - email = Mail::Address.new(article['from']).address - rescue - email = article['from'] - if article['from'] =~ /<(.+?)>/ - email = $1 - end - end - - user = User.lookup(email: email) - if !user - user = User.lookup(login: email) - end - if !user - begin - display_name = Mail::Address.new( article['from'] ).display_name || - ( Mail::Address.new( article['from'] ).comments && Mail::Address.new( article['from'] ).comments[0] ) - rescue - display_name = article['from'] - end - - # do extra decoding because we needed to use field.value - display_name = Mail::Field.new('X-From', display_name).to_s - - roles = Role.lookup(name: 'Customer') - begin - user = User.create( - login: email, - firstname: display_name, - lastname: '', - email: email, - password: '', - active: true, - role_ids: [roles.id], - updated_by_id: 1, - created_by_id: 1, + break if !imported?( + remote_object: remote_object, + limit: limit, + offset: offset, + diff: args[:diff] ) - rescue ActiveRecord::RecordNotUnique - log "User #{email} was handled by another thread, taking this." - user = User.lookup(login: email) - if !user - log "User #{email} wasn't created sleep and retry." - sleep rand 3 - retry - end + + offset += limit end end - article['created_by_id'] = user.id - true + def imported?(args) + log "loading #{args[:limit]} #{args[:remote_object]} starting at #{args[:offset]}..." + return false if !import_action(args[:remote_object], limit: args[:limit], offset: args[:offset], diff: args[:diff]) + true + end + + def import_action(remote_object, args = {}) + records = Import::OTRS::Requester.load(remote_object, limit: args[:limit], offset: args[:offset], diff: args[:diff]) + if !records || records.empty? + log '... no more work.' + return false + end + factory_class(remote_object).import(records) + end + + def factory_class(object) + "Import::OTRS::#{object}Factory".constantize + end + + # sync settings + def base_objects + import('SysConfig') + import('DynamicField') + end + + def updateable_objects + import('State') + import('Priority') + import('Queue') + import('User') + import('Customer') + end + + def customer_user + limit_import('CustomerUser', limit: 50) + end end - - def self.skip_fields - %w(ProcessManagementProcessID ProcessManagementActivityID ZammadMigratorChanged ZammadMigratorChangedOld) - end - - def self._first_customer_id(articles) - user_id = 1 - articles.each { |article| - next if article['sender'] != 'customer' - next if article['from'].empty? - - user_id = article['created_by_id'].to_i - break - } - - user_id - end -end +end \ No newline at end of file diff --git a/lib/import/otrs/article.rb b/lib/import/otrs/article.rb new file mode 100644 index 000000000..bef778d82 --- /dev/null +++ b/lib/import/otrs/article.rb @@ -0,0 +1,136 @@ +module Import + module OTRS + class Article + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + TicketID: :ticket_id, + ArticleID: :id, + Body: :body, + From: :from, + To: :to, + Cc: :cc, + Subject: :subject, + InReplyTo: :in_reply_to, + MessageID: :message_id, + #ReplyTo: :reply_to, + References: :references, + Changed: :updated_at, + Created: :created_at, + ChangedBy: :updated_by_id, + CreatedBy: :created_by_id, + }.freeze + + def initialize(article) + initialize_article_sender_types + initialize_article_types + + utf8_encode(article) + import(article) + end + + private + + def import(article) + create_or_update(map(article)) + + return if !article['Attachments'] + return if article['Attachments'].empty? + + Import::OTRS::Article::AttachmentFactory.import( + attachments: article['Attachments'], + local_article: @local_article + ) + end + + def create_or_update(article) + return if updated?(article) + create(article) + end + + def updated?(article) + @local_article = ::Ticket::Article.find_by(id: article[:id]) + return false if !@local_article + log "update Ticket::Article.find_by(id: #{article[:id]})" + @local_article.update_attributes(article) + true + end + + def create(article) + log "add Ticket::Article.find_by(id: #{article[:id]})" + @local_article = ::Ticket::Article.new(article) + @local_article.id = article[:id] + @local_article.save + reset_primary_key_sequence('ticket_articles') + rescue ActiveRecord::RecordNotUnique + log "Ticket #{article[:ticket_id]} (article #{article[:id]}) is handled by another thead, skipping." + end + + def map(article) + { + created_by_id: 1, + updated_by_id: 1, + } + .merge(from_mapping(article)) + .merge(article_type(article)) + .merge(article_sender_type(article)) + end + + def article_type(article) + @article_types[article['ArticleType']] || @article_types['note-internal'] + end + + def article_sender_type(article) + { + sender_id: @sender_type_id[article['SenderType']] || @sender_type_id['note-internal'] + } + end + + def initialize_article_sender_types + @sender_type_id = { + 'customer' => article_sender_type_id_lookup('Customer'), + 'agent' => article_sender_type_id_lookup('Agent'), + 'system' => article_sender_type_id_lookup('System'), + } + end + + def article_sender_type_id_lookup(name) + ::Ticket::Article::Sender.find_by(name: name).id + end + + def initialize_article_types + @article_types = { + 'email-external' => { + type_id: article_type_id_lookup('email'), + internal: false + }, + 'email-internal' => { + type_id: article_type_id_lookup('email'), + internal: true + }, + 'note-external' => { + type_id: article_type_id_lookup('note'), + internal: false + }, + 'note-internal' => { + type_id: article_type_id_lookup('note'), + internal: true + }, + 'phone' => { + type_id: article_type_id_lookup('phone'), + internal: false + }, + 'webrequest' => { + type_id: article_type_id_lookup('web'), + internal: false + }, + } + end + + def article_type_id_lookup(name) + ::Ticket::Article::Type.lookup(name: name).id + end + end + end +end diff --git a/lib/import/otrs/article/attachment_factory.rb b/lib/import/otrs/article/attachment_factory.rb new file mode 100644 index 000000000..ad44e663d --- /dev/null +++ b/lib/import/otrs/article/attachment_factory.rb @@ -0,0 +1,104 @@ +module Import + module OTRS + class Article + module AttachmentFactory + extend Import::Helper + + # rubocop:disable Style/ModuleFunction + extend self + + def import(args) + attachments = args[:attachments] || [] + local_article = args[:local_article] + + return if skip_import?(attachments, local_article) + perform_import(attachments, local_article) + end + + private + + def perform_import(attachments, local_article) + attachments.each { |attachment| import_single(local_article, attachment) } + end + + def import_single(local_article, attachment) + + decoded_filename = Base64.decode64(attachment['Filename']) + decoded_content = Base64.decode64(attachment['Content']) + # TODO: should be done by a/the Storage object + # to handle fingerprinting + sha = Digest::SHA256.hexdigest(decoded_content) + + retries = 3 + begin + queueing(sha, decoded_filename) + + log "Ticket #{local_article.ticket_id}, Article #{local_article.id} - Starting import for fingerprint #{sha} (#{decoded_filename})... Queue: #{@sha_queue[sha]}." + ActiveRecord::Base.transaction do + Store.add( + object: 'Ticket::Article', + o_id: local_article.id, + filename: decoded_filename, + data: decoded_content, + preferences: { + 'Mime-Type' => attachment['ContentType'], + 'Content-ID' => attachment['ContentID'], + 'content-alternative' => attachment['ContentAlternative'], + }, + created_by_id: 1, + ) + end + log "Ticket #{local_article.ticket_id}, Article #{local_article.id} - Finished import for fingerprint #{sha} (#{decoded_filename})... Queue: #{@sha_queue[sha]}." + rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid => e + log "Ticket #{local_article.ticket_id} - #{sha} - #{e.class}: #{e}" + sleep rand 3 + retry if !(retries -= 1).zero? + raise + ensure + queue_cleanup(sha) + end + end + + def skip_import?(attachments, local_article) + local_attachments = local_article.attachments + return true if local_attachments.count == attachments.count + # get a common ground + local_attachments.each(&:delete) + return true if attachments.empty? + false + end + + def queueing(sha, decoded_filename) + # this is (currently) needed for avoiding + # race conditions inserting attachments with + # the same fingerprint in the DB in concurrent threads + @sha_queue ||= {} + @sha_queue[sha] ||= [] + + return if !queueing_active? + @sha_queue[sha].push(queue_id) + + while @sha_queue[sha].first != queue_id + sleep_time = 0.25 + log "Found active import for fingerprint #{sha} (#{decoded_filename})... sleeping #{sleep_time} seconds. Queue: #{@sha_queue[sha]}." + sleep sleep_time + end + end + + def queue_cleanup(sha) + return if !queueing_active? + @sha_queue[sha].shift + end + + def queueing_active? + return if !queue_id + true + end + + def queue_id + Thread.current[:thread_no] + end + end + end + end +end diff --git a/lib/import/otrs/article_customer.rb b/lib/import/otrs/article_customer.rb new file mode 100644 index 000000000..330ca164a --- /dev/null +++ b/lib/import/otrs/article_customer.rb @@ -0,0 +1,84 @@ +module Import + module OTRS + class ArticleCustomer + include Import::Helper + + def initialize(article) + user = import(article) + return if !user + article['created_by_id'] = user.id + rescue Exceptions::UnprocessableEntity => e + log "ERROR: Can't extract customer from Article #{article[:id]}" + end + + private + + def import(article) + find_user_or_create(article) + end + + def extract_email(from) + Mail::Address.new(from).address + rescue + return from if from !~ /<\s*([^\s]+)/ + $1 + end + + def find_user_or_create(article) + user = user_found?(article) + return user if user + create_user(article) + end + + def user_found?(article) + email = extract_email(article['From']) + user = ::User.find_by(email: email) + user ||= ::User.find_by(login: email) + user + end + + def create_user(article) + email = extract_email(article['From']) + ::User.create( + login: email, + firstname: extract_display_name(article['from']), + lastname: '', + email: email, + password: '', + active: true, + role_ids: roles, + updated_by_id: 1, + created_by_id: 1, + ) + rescue ActiveRecord::RecordNotUnique + log "User #{email} was handled by another thread, taking this." + + return if user_found?(article) + + log "User #{email} wasn't created sleep and retry." + sleep rand 3 + retry + end + + def roles + [ + Role.find_by(name: 'Customer').id + ] + end + + def extract_display_name(from) + # do extra decoding because we needed to use field.value + Mail::Field.new('X-From', parsed_display_name(from)).to_s + end + + def parsed_display_name(from) + parsed_address = Mail::Address.new(from) + return parsed_address.display_name if parsed_address.display_name + return from if parsed_address.comments.empty? + parsed_address.comments[0] + rescue + from + end + end + end +end diff --git a/lib/import/otrs/article_customer_factory.rb b/lib/import/otrs/article_customer_factory.rb new file mode 100644 index 000000000..1ab64fe58 --- /dev/null +++ b/lib/import/otrs/article_customer_factory.rb @@ -0,0 +1,14 @@ +module Import + module OTRS + module ArticleCustomerFactory + extend Import::Factory + + def skip?(record) + return true if record['sender'] != 'customer' + return true if record['created_by_id'].to_i != 1 + return true if record['from'].empty? + false + end + end + end +end diff --git a/lib/import/otrs/article_factory.rb b/lib/import/otrs/article_factory.rb new file mode 100644 index 000000000..caf2a55bf --- /dev/null +++ b/lib/import/otrs/article_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module ArticleFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/async.rb b/lib/import/otrs/async.rb new file mode 100644 index 000000000..fd54b1a3a --- /dev/null +++ b/lib/import/otrs/async.rb @@ -0,0 +1,62 @@ +module Import + module OTRS + module Async + # rubocop:disable Style/ModuleFunction + extend self + + def start_bg + Setting.reload + + Import::OTRS::Requester.connection_test + + # 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::OTRS.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/otrs/customer.rb b/lib/import/otrs/customer.rb new file mode 100644 index 000000000..9a3d31e1a --- /dev/null +++ b/lib/import/otrs/customer.rb @@ -0,0 +1,67 @@ +module Import + module OTRS + class Customer + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + CustomerCompanyName: :name, + CustomerCompanyComment: :note, + }.freeze + + def initialize(customer) + import(customer) + end + + def self.by_customer_id(customer_id) + organizations = Import::OTRS::Requester.load('Customer') + + result = nil + organizations.each do |organization| + next if customer_id != organization['CustomerID'] + result = Organization.find_by(name: organization['CustomerCompanyName']) + break + end + result + end + + private + + def import(customer) + create_or_update(map(customer)) + end + + def create_or_update(customer) + return if updated?(customer) + create(customer) + end + + def updated?(customer) + @local_customer = Organization.find_by(name: customer[:name]) + return false if !@local_customer + log "update Organization.find_by(name: #{customer[:name]})" + @local_customer.update_attributes(customer) + true + end + + def create(customer) + log "add Organization.find_by(name: #{customer[:name]})" + @local_customer = Organization.create(customer) + reset_primary_key_sequence('organizations') + end + + def map(customer) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(customer), + } + .merge(from_mapping(customer)) + end + end + end +end diff --git a/lib/import/otrs/customer_factory.rb b/lib/import/otrs/customer_factory.rb new file mode 100644 index 000000000..1c2c36f46 --- /dev/null +++ b/lib/import/otrs/customer_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module CustomerFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/customer_user.rb b/lib/import/otrs/customer_user.rb new file mode 100644 index 000000000..1d98ba88d --- /dev/null +++ b/lib/import/otrs/customer_user.rb @@ -0,0 +1,92 @@ +module Import + module OTRS + class CustomerUser + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + UserComment: :note, + UserEmail: :email, + UserFirstname: :firstname, + UserLastname: :lastname, + UserLogin: :login, + UserPassword: :password, + UserPhone: :phone, + UserFax: :fax, + UserMobile: :mobile, + UserStreet: :street, + UserZip: :zip, + UserCity: :city, + UserCountry: :country, + }.freeze + + def initialize(customer) + import(customer) + end + + private + + def import(customer) + create_or_update(map(customer)) + end + + def create_or_update(customer) + return if updated?(customer) + create(customer) + end + + def updated?(customer) + @local_customer = ::User.find_by(login: customer[:login]) + return false if !@local_customer + + # do not update user if it is already agent + return true if @local_customer.role_ids.include?(Role.find_by(name: 'Agent').id) + + # only update roles if different (reduce sql statements) + if @local_customer.role_ids == customer[:role_ids] + user.delete(:role_ids) + end + + log "update User.find_by(login: #{customer[:login]})" + @local_customer.update_attributes(customer) + true + end + + def create(customer) + log "add User.find_by(login: #{customer[:login]})" + @local_customer = ::User.new(customer) + @local_customer.save + reset_primary_key_sequence('users') + end + + def map(customer) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(customer), + source: 'OTRS Import', + organization_id: organization_id(customer), + role_ids: role_ids, + } + .merge(from_mapping(customer)) + end + + def role_ids + [ + Role.find_by(name: 'Customer').id + ] + end + + def organization_id(customer) + return if !customer['UserCustomerID'] + organization = Import::OTRS::Customer.by_customer_id(customer['UserCustomerID']) + return if !organization + organization.id + end + end + end +end diff --git a/lib/import/otrs/customer_user_factory.rb b/lib/import/otrs/customer_user_factory.rb new file mode 100644 index 000000000..02ee37daf --- /dev/null +++ b/lib/import/otrs/customer_user_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module CustomerUserFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/diff.rb b/lib/import/otrs/diff.rb new file mode 100644 index 000000000..096c223f3 --- /dev/null +++ b/lib/import/otrs/diff.rb @@ -0,0 +1,36 @@ +module Import + module OTRS + module Diff + # rubocop:disable Style/ModuleFunction + extend self + + def diff_worker + return if !diff_import_possible? + diff + end + + private + + def diff_import_possible? + return if !Setting.get('import_mode') + return if Setting.get('import_otrs_endpoint') == 'http://otrs_host/otrs' + true + end + + def diff + log 'Start diff...' + + check_import_mode + + updateable_objects + + # get changed tickets + ticket_diff + end + + def ticket_diff + import_regular('Ticket', diff: true) + end + end + end +end diff --git a/lib/import/otrs/dynamic_field.rb b/lib/import/otrs/dynamic_field.rb new file mode 100644 index 000000000..b108d6aa6 --- /dev/null +++ b/lib/import/otrs/dynamic_field.rb @@ -0,0 +1,61 @@ +module Import + module OTRS + class DynamicField + + def initialize(dynamic_field) + @internal_name = self.class.convert_name(dynamic_field['Name']) + + return if already_imported?(dynamic_field) + + initialize_attribute_config(dynamic_field) + + init_callback(dynamic_field) + add + end + + def self.convert_name(dynamic_field_name) + dynamic_field_name.underscore.sub(/\_id(s)?\z/, '_no\1') + end + + private + + def init_callback(_) + raise 'No init callback defined for this dynamic field!' + end + + def already_imported?(dynamic_field) + attribute = ObjectManager::Attribute.get( + object: dynamic_field['ObjectType'], + name: @internal_name, + ) + attribute ? true : false + end + + def initialize_attribute_config(dynamic_field) + + @attribute_config = { + object: dynamic_field['ObjectType'], + name: @internal_name, + display: dynamic_field['Label'], + screens: { + view: { + '-all-' => { + shown: true, + }, + }, + }, + active: true, + editable: dynamic_field['InternalField'] == '0', + position: dynamic_field['FieldOrder'], + created_by_id: 1, + updated_by_id: 1, + } + end + + def add + ObjectManager::Attribute.add(@attribute_config) + ObjectManager::Attribute.migration_execute(false) + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/checkbox.rb b/lib/import/otrs/dynamic_field/checkbox.rb new file mode 100644 index 000000000..8666986ac --- /dev/null +++ b/lib/import/otrs/dynamic_field/checkbox.rb @@ -0,0 +1,22 @@ +module Import + module OTRS + class DynamicField + class Checkbox < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'boolean', + data_option: { + default: dynamic_field['Config']['DefaultValue'] == '1', + options: { + true => 'Yes', + false => 'No', + }, + null: false, + translate: true, + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/date.rb b/lib/import/otrs/dynamic_field/date.rb new file mode 100644 index 000000000..6be4508bd --- /dev/null +++ b/lib/import/otrs/dynamic_field/date.rb @@ -0,0 +1,24 @@ +# this require is required (hehe) because of Rails autoloading +# which causes strange behavior not inheriting correctly +# from Import::OTRS::DynamicField +require 'import/otrs/dynamic_field' + +module Import + module OTRS + class DynamicField + class Date < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'date', + data_option: { + future: dynamic_field['Config']['YearsInFuture'] != '0', + past: dynamic_field['Config']['YearsInPast'] != '0', + diff: dynamic_field['Config']['DefaultValue'].to_i / 60 / 60 / 24, + null: false, + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/date_time.rb b/lib/import/otrs/dynamic_field/date_time.rb new file mode 100644 index 000000000..997a4ac49 --- /dev/null +++ b/lib/import/otrs/dynamic_field/date_time.rb @@ -0,0 +1,24 @@ +# this require is required (hehe) because of Rails autoloading +# which causes strange behavior not inheriting correctly +# from Import::OTRS::DynamicField +require 'import/otrs/dynamic_field' + +module Import + module OTRS + class DynamicField + class DateTime < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'datetime', + data_option: { + future: dynamic_field['Config']['YearsInFuture'] != '0', + past: dynamic_field['Config']['YearsInPast'] != '0', + diff: dynamic_field['Config']['DefaultValue'].to_i / 60 / 60, + null: false, + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/dropdown.rb b/lib/import/otrs/dynamic_field/dropdown.rb new file mode 100644 index 000000000..a6d0d8c5a --- /dev/null +++ b/lib/import/otrs/dynamic_field/dropdown.rb @@ -0,0 +1,20 @@ +module Import + module OTRS + class DynamicField + class Dropdown < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'select', + data_option: { + default: '', + multiple: false, + options: dynamic_field['Config']['PossibleValues'], + null: dynamic_field['Config']['PossibleNone'] == '1', + translate: dynamic_field['Config']['TranslatableValues'] == '1', + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/multiselect.rb b/lib/import/otrs/dynamic_field/multiselect.rb new file mode 100644 index 000000000..3710fc7d0 --- /dev/null +++ b/lib/import/otrs/dynamic_field/multiselect.rb @@ -0,0 +1,20 @@ +module Import + module OTRS + class DynamicField + class Multiselect < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'select', + data_option: { + default: '', + multiple: true, + options: dynamic_field['Config']['PossibleValues'], + null: dynamic_field['Config']['PossibleNone'] == '1', + translate: dynamic_field['Config']['TranslatableValues'] == '1', + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/text.rb b/lib/import/otrs/dynamic_field/text.rb new file mode 100644 index 000000000..cafcb7f41 --- /dev/null +++ b/lib/import/otrs/dynamic_field/text.rb @@ -0,0 +1,19 @@ +module Import + module OTRS + class DynamicField + class Text < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'input', + data_option: { + default: dynamic_field['Config']['DefaultValue'], + type: 'text', + maxlength: 255, + null: false, + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/text_area.rb b/lib/import/otrs/dynamic_field/text_area.rb new file mode 100644 index 000000000..e4b71ce72 --- /dev/null +++ b/lib/import/otrs/dynamic_field/text_area.rb @@ -0,0 +1,18 @@ +module Import + module OTRS + class DynamicField + class TextArea < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'textarea', + data_option: { + default: dynamic_field['Config']['DefaultValue'], + rows: dynamic_field['Config']['Rows'], + null: false, + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field_factory.rb b/lib/import/otrs/dynamic_field_factory.rb new file mode 100644 index 000000000..f6bbf350e --- /dev/null +++ b/lib/import/otrs/dynamic_field_factory.rb @@ -0,0 +1,52 @@ +module Import + module OTRS + module DynamicFieldFactory + extend Import::Factory + extend Import::Helper + + # rubocop:disable Style/ModuleFunction + extend self + + def skip?(record) + return true if !importable?(record) + return true if skip_field?(record['Name']) + false + end + + def backend_class(record) + "Import::OTRS::DynamicField::#{record['FieldType']}".constantize + end + + def skip_field?(dynamic_field_name) + skip_fields.include?(dynamic_field_name) + end + + private + + def importable?(dynamic_field) + return false if !supported_object_type?(dynamic_field) + supported_field_type?(dynamic_field) + end + + def supported_object_type?(dynamic_field) + return true if dynamic_field['ObjectType'] == 'Ticket' + log "ERROR: Unsupported dynamic field object type '#{dynamic_field['ObjectType']}' for dynamic field '#{dynamic_field['Name']}'" + false + end + + def supported_field_type?(dynamic_field) + return true if supported_field_types.include?(dynamic_field['FieldType']) + log "ERROR: Unsupported dynamic field field type '#{dynamic_field['FieldType']}' for dynamic field '#{dynamic_field['Name']}'" + false + end + + def supported_field_types + %w(Text TextArea Checkbox DateTime Date Dropdown Multiselect) + end + + def skip_fields + %w(ProcessManagementProcessID ProcessManagementActivityID ZammadMigratorChanged ZammadMigratorChangedOld) + end + end + end +end diff --git a/lib/import/otrs/helper.rb b/lib/import/otrs/helper.rb new file mode 100644 index 000000000..107b74956 --- /dev/null +++ b/lib/import/otrs/helper.rb @@ -0,0 +1,37 @@ +module Import + module OTRS + module Helper + # rubocop:disable Style/ModuleFunction + extend self + + private + + def from_mapping(record) + result = {} + # use the mapping of the class in which + # this module gets extended + self.class::MAPPING.each { |key_sym, value| + key = key_sym.to_s + next if !record.key?(key) + result[value] = record[key] + } + result + end + + def active?(record) + case record['ValidID'].to_s + when '3' + false + when '2' + false + when '1' + true + when '0' + false + else + true + end + end + end + end +end diff --git a/lib/import/otrs/history.rb b/lib/import/otrs/history.rb new file mode 100644 index 000000000..742bca834 --- /dev/null +++ b/lib/import/otrs/history.rb @@ -0,0 +1,21 @@ +module Import + module OTRS + class History + + def initialize(history) + init_callback(history) + add + end + + def init_callback(_) + raise 'No init callback defined for this history!' + end + + private + + def add + ::History.add(@history_attributes) + end + end + end +end diff --git a/lib/import/otrs/history/article.rb b/lib/import/otrs/history/article.rb new file mode 100644 index 000000000..4145f7987 --- /dev/null +++ b/lib/import/otrs/history/article.rb @@ -0,0 +1,20 @@ +module Import + module OTRS + class History + class Article < Import::OTRS::History + def init_callback(history) + @history_attributes = { + id: history['HistoryID'], + o_id: history['ArticleID'], + history_type: 'created', + history_object: 'Ticket::Article', + related_o_id: history['TicketID'], + related_history_object: 'Ticket', + created_at: history['CreateTime'], + created_by_id: history['CreateBy'] + } + end + end + end + end +end diff --git a/lib/import/otrs/history/move.rb b/lib/import/otrs/history/move.rb new file mode 100644 index 000000000..b328e875e --- /dev/null +++ b/lib/import/otrs/history/move.rb @@ -0,0 +1,33 @@ +module Import + module OTRS + class History + class Move < Import::OTRS::History + def init_callback(history) + data = history['Name'] + # "%%Queue1%%5%%Postmaster%%1" + from = nil + to = nil + if data =~ /%%(.+?)%%(.+?)%%(.+?)%%(.+?)$/ + from = $1 + from_id = $2 + to = $3 + to_id = $4 + end + @history_attributes = { + id: history['HistoryID'], + o_id: history['TicketID'], + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'group', + value_from: from, + value_to: to, + id_from: from_id, + id_to: to_id, + created_at: history['CreateTime'], + created_by_id: history['CreateBy'] + } + end + end + end + end +end diff --git a/lib/import/otrs/history/new_ticket.rb b/lib/import/otrs/history/new_ticket.rb new file mode 100644 index 000000000..5459f6ba2 --- /dev/null +++ b/lib/import/otrs/history/new_ticket.rb @@ -0,0 +1,18 @@ +module Import + module OTRS + class History + class NewTicket < Import::OTRS::History + def init_callback(history) + @history_attributes = { + id: history['HistoryID'], + o_id: history['TicketID'], + history_type: 'created', + history_object: 'Ticket', + created_at: history['CreateTime'], + created_by_id: history['CreateBy'] + } + end + end + end + end +end diff --git a/lib/import/otrs/history/priority_update.rb b/lib/import/otrs/history/priority_update.rb new file mode 100644 index 000000000..4659c148a --- /dev/null +++ b/lib/import/otrs/history/priority_update.rb @@ -0,0 +1,33 @@ +module Import + module OTRS + class History + class PriorityUpdate < Import::OTRS::History + def init_callback(history) + data = history['Name'] + # "%%3 normal%%3%%5 very high%%5" + from = nil + to = nil + if data =~ /%%(.+?)%%(.+?)%%(.+?)%%(.+?)$/ + from = $1 + from_id = $2 + to = $3 + to_id = $4 + end + @history_attributes = { + id: history['HistoryID'], + o_id: history['TicketID'], + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'priority', + value_from: from, + value_to: to, + id_from: from_id, + id_to: to_id, + created_at: history['CreateTime'], + created_by_id: history['CreateBy'] + } + end + end + end + end +end diff --git a/lib/import/otrs/history/state_update.rb b/lib/import/otrs/history/state_update.rb new file mode 100644 index 000000000..a1b7ce280 --- /dev/null +++ b/lib/import/otrs/history/state_update.rb @@ -0,0 +1,39 @@ +module Import + module OTRS + class History + class StateUpdate < Import::OTRS::History + def init_callback(history) + data = history['Name'] + # "%%new%%open%%" + from = nil + to = nil + if data =~ /%%(.+?)%%(.+?)%%/ + from = $1 + to = $2 + state_from = ::Ticket::State.lookup(name: from) + state_to = ::Ticket::State.lookup(name: to) + if state_from + from_id = state_from.id + end + if state_to + to_id = state_to.id + end + end + @history_attributes = { + id: history['HistoryID'], + o_id: history['TicketID'], + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'state', + value_from: from, + id_from: from_id, + value_to: to, + id_to: to_id, + created_at: history['CreateTime'], + created_by_id: history['CreateBy'] + } + end + end + end + end +end diff --git a/lib/import/otrs/history_factory.rb b/lib/import/otrs/history_factory.rb new file mode 100644 index 000000000..3d91b66e9 --- /dev/null +++ b/lib/import/otrs/history_factory.rb @@ -0,0 +1,40 @@ +module Import + module OTRS + module HistoryFactory + extend Import::Factory + + # rubocop:disable Style/ModuleFunction + extend self + + def skip?(record) + return true if !determine_class(record) + false + end + + def backend_class(record) + "Import::OTRS::History::#{determine_class(record)}".constantize + end + + private + + def determine_class(history) + check_supported(history) || check_article(history) + end + + def supported_types + %w(NewTicket StateUpdate Move PriorityUpdate) + end + + def check_supported(history) + return if !supported_types.include?(history['HistoryType']) + history['HistoryType'] + end + + def check_article(history) + return if !history['ArticleID'] + return if history['ArticleID'].to_i.zero? + 'Article' + end + end + end +end diff --git a/lib/import/otrs/import_stats.rb b/lib/import/otrs/import_stats.rb new file mode 100644 index 000000000..e07e5609b --- /dev/null +++ b/lib/import/otrs/import_stats.rb @@ -0,0 +1,74 @@ +module Import + module OTRS + module ImportStats + # rubocop:disable Style/ModuleFunction + extend self + + def current_state + { + Base: { + done: base_done, + total: base_total, + }, + User: { + done: user_done, + total: user_total, + }, + Ticket: { + done: ticket_done, + total: ticket_total, + }, + } + end + + private + + def statistic + + # check cache + cache = Cache.get('import_otrs_stats') + return cache if cache + + # retrive statistic + statistic = Import::OTRS::Requester.list + return statistic if !statistic + + Cache.write('import_otrs_stats', statistic) + statistic + end + + def base_done + Group.count + ::Ticket::State.count + ::Ticket::Priority.count + end + + def base_total + sum_stat(%w(Queue State Priority)) + end + + def user_done + User.count + end + + def user_total + sum_stat(%w(User CustomerUser)) + end + + def ticket_done + Ticket.count + end + + def ticket_total + sum_stat(%w(Ticket)) + end + + def sum_stat(objects) + data = statistic + sum = 0 + objects.each { |object| + sum += data[object] + } + sum + end + end + end +end diff --git a/lib/import/otrs/priority.rb b/lib/import/otrs/priority.rb new file mode 100644 index 000000000..8882a7ad9 --- /dev/null +++ b/lib/import/otrs/priority.rb @@ -0,0 +1,58 @@ +module Import + module OTRS + class Priority + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + Name: :name, + ID: :id, + Comment: :note, + }.freeze + + def initialize(priority) + import(priority) + end + + private + + def import(priority) + create_or_update(map(priority)) + end + + def create_or_update(priority) + return if updated?(priority) + create(priority) + end + + def updated?(priority) + @local_priority = ::Ticket::Priority.find_by(id: priority[:id]) + return false if !@local_priority + log "update Ticket::Priority.find_by(id: #{priority[:id]})" + @local_priority.update_attributes(priority) + true + end + + def create(priority) + log "add Ticket::Priority.find_by(id: #{priority[:id]})" + @local_priority = ::Ticket::Priority.new(priority) + @local_priority.id = priority[:id] + @local_priority.save + reset_primary_key_sequence('ticket_priorities') + end + + def map(priority) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(priority), + } + .merge(from_mapping(priority)) + end + end + end +end diff --git a/lib/import/otrs/priority_factory.rb b/lib/import/otrs/priority_factory.rb new file mode 100644 index 000000000..ebd405775 --- /dev/null +++ b/lib/import/otrs/priority_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module PriorityFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/queue.rb b/lib/import/otrs/queue.rb new file mode 100644 index 000000000..a00e4531a --- /dev/null +++ b/lib/import/otrs/queue.rb @@ -0,0 +1,58 @@ +module Import + module OTRS + class Queue + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + Name: :name, + QueueID: :id, + Comment: :note, + }.freeze + + def initialize(queue) + import(queue) + end + + private + + def import(queue) + create_or_update(map(queue)) + end + + def create_or_update(queue) + return if updated?(queue) + create(queue) + end + + def updated?(queue) + @local_queue = Group.find_by(id: queue[:id]) + return false if !@local_queue + log "update Group.find_by(id: #{queue[:id]})" + @local_queue.update_attributes(queue) + true + end + + def create(queue) + log "add Group.find_by(id: #{queue[:id]})" + @local_queue = Group.new(queue) + @local_queue.id = queue[:id] + @local_queue.save + reset_primary_key_sequence('groups') + end + + def map(queue) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(queue), + } + .merge(from_mapping(queue)) + end + end + end +end diff --git a/lib/import/otrs/queue_factory.rb b/lib/import/otrs/queue_factory.rb new file mode 100644 index 000000000..b2ec3ba35 --- /dev/null +++ b/lib/import/otrs/queue_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module QueueFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/requester.rb b/lib/import/otrs/requester.rb new file mode 100644 index 000000000..69b1d4fb2 --- /dev/null +++ b/lib/import/otrs/requester.rb @@ -0,0 +1,93 @@ +module Import + module OTRS + module Requester + extend Import::Helper + + # rubocop:disable Style/ModuleFunction + extend self + + def load(object, args = {}) + + @cache ||= {} + if args.empty? && @cache[object] + return @cache[object] + end + + result = request_result( + Subaction: 'Export', + Object: object, + Limit: args[:limit] || '', + Offset: args[:offset] || '', + Diff: args[:diff] ? 1 : 0 + ) + + return result if !args.empty? + @cache[object] = result + @cache[object] + end + + def list + request_result(Subaction: 'List') + end + + # TODO: refactor to something like .connected? + def connection_test + result = request_json({}) + return true if result['Success'] + raise 'API key not valid!' + end + + private + + def request_result(params) + response = request_json(params) + response['Result'] + end + + def request_json(params) + response = post(params) + result = handle_response(response) + + return result if result + + raise 'Invalid response' + end + + def handle_response(response) + encoded_body = Encode.conv('utf8', response.body.to_s) + JSON.parse(encoded_body) + end + + def post(params) + url = Setting.get('import_otrs_endpoint') + params[:Action] = 'ZammadMigrator' + params[:Key] = Setting.get('import_otrs_endpoint_key') + + log 'POST: ' + url + log 'PARAMS: ' + params.inspect + + response = UserAgent.post( + url, + params, + { + open_timeout: 10, + read_timeout: 120, + total_timeout: 360, + user: Setting.get('import_otrs_user'), + password: Setting.get('import_otrs_password'), + }, + ) + + if !response + raise "Can't connect to Zammad Migrator" + end + + if !response.success? + log "ERROR: #{response.error}" + raise 'Zammad Migrator returned an error' + end + response + end + end + end +end diff --git a/lib/import/otrs/state.rb b/lib/import/otrs/state.rb new file mode 100644 index 000000000..3841e441e --- /dev/null +++ b/lib/import/otrs/state.rb @@ -0,0 +1,70 @@ +module Import + module OTRS + class State + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + Name: :name, + ID: :id, + ValidID: :active, + Comment: :note, + }.freeze + + def initialize(state) + import(state) + end + + private + + def import(state) + create_or_update(map(state)) + end + + def create_or_update(state) + return if updated?(state) + create(state) + end + + def updated?(state) + @local_state = ::Ticket::State.find_by(id: state[:id]) + return false if !@local_state + log "update Ticket::State.find_by(id: #{state[:id]})" + @local_state.update_attributes(state) + true + end + + def create(state) + log "add Ticket::State.find_by(id: #{state[:id]})" + @local_state = ::Ticket::State.new(state) + @local_state.id = state[:id] + @local_state.save + reset_primary_key_sequence('ticket_states') + end + + def map(state) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(state), + state_type_id: state_type_id(state) + } + .merge(from_mapping(state)) + end + + def state_type_id(state) + map_type(state) + ::Ticket::StateType.lookup(name: state['TypeName']).id + end + + def map_type(state) + return if state['TypeName'] != 'pending auto' + state['TypeName'] = 'pending action' + end + end + end +end diff --git a/lib/import/otrs/state_factory.rb b/lib/import/otrs/state_factory.rb new file mode 100644 index 000000000..40b85c8b1 --- /dev/null +++ b/lib/import/otrs/state_factory.rb @@ -0,0 +1,22 @@ +module Import + module OTRS + module StateFactory + extend Import::TransactionFactory + + # rubocop:disable Style/ModuleFunction + extend self + + def pre_import_hook(_records) + backup + end + + def backup + # rename states to handle not uniq issues + ::Ticket::State.all.each { |state| + state.name = state.name + '_tmp' + state.save + } + end + end + end +end diff --git a/lib/import/otrs/sys_config_factory.rb b/lib/import/otrs/sys_config_factory.rb new file mode 100644 index 000000000..3dd945bc3 --- /dev/null +++ b/lib/import/otrs/sys_config_factory.rb @@ -0,0 +1,51 @@ +module Import + module OTRS + module SysConfigFactory + + # rubocop:disable Style/ModuleFunction + extend self + + def import(settings) + settings.each do |setting| + next if direct_copy?(setting) + next if number_generator?(setting) + end + end + + private + + def direct_settings + %w(FQDN HttpType SystemID Organization TicketHook) + end + + def direct_copy?(setting) + cleaned_name = cleanup_name(setting['Key']) + return false if !direct_settings.include?(cleaned_name) + + internal_name = cleaned_name.underscore + Setting.set(internal_name, setting['Value']) + + true + end + + def cleanup_name(key) + key.tr('::', '') + end + + def number_generator?(setting) + return false if setting['Key'] != 'Ticket::NumberGenerator' + + case setting['Value'] + when 'Kernel::System::Ticket::Number::DateChecksum' + Setting.set('ticket_number', 'Ticket::Number::Date') + Setting.set('ticket_number_date', { checksum: true }) + when 'Kernel::System::Ticket::Number::Date' + Setting.set('ticket_number', 'Ticket::Number::Date') + Setting.set('ticket_number_date', { checksum: false }) + end + + true + end + end + end +end diff --git a/lib/import/otrs/ticket.rb b/lib/import/otrs/ticket.rb new file mode 100644 index 000000000..5e2841d0e --- /dev/null +++ b/lib/import/otrs/ticket.rb @@ -0,0 +1,163 @@ +require 'ticket' + +module Import + module OTRS + class Ticket + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + Changed: :updated_at, + Created: :created_at, + CreateBy: :created_by_id, + TicketNumber: :number, + QueueID: :group_id, + StateID: :state_id, + PriorityID: :priority_id, + Title: :title, + TicketID: :id, + FirstResponse: :first_response_at, + #FirstResponseTimeDestinationDate: :first_response_escalation_at, + #FirstResponseInMin: :first_response_in_min, + #FirstResponseDiffInMin: :first_response_diff_in_min, + Closed: :close_at, + #SoltutionTimeDestinationDate: :close_escalation_at, + #CloseTimeInMin: :close_in_min, + #CloseTimeDiffInMin: :close_diff_in_min, + }.freeze + + def initialize(ticket) + fix(ticket) + import(ticket) + end + + private + + def import(ticket) + Import::OTRS::ArticleCustomerFactory.import(ticket['Articles']) + + create_or_update(map(ticket)) + + Import::OTRS::ArticleFactory.import(ticket['Articles']) + Import::OTRS::HistoryFactory.import(ticket['History']) + end + + def create_or_update(ticket) + return if updated?(ticket) + create(ticket) + end + + def updated?(ticket) + @local_ticket = ::Ticket.find_by(id: ticket[:id]) + return false if !@local_ticket + log "update Ticket.find_by(id: #{ticket[:id]})" + @local_ticket.update_attributes(ticket) + true + end + + def create(ticket) + log "add Ticket.find_by(id: #{ticket[:id]})" + @local_ticket = ::Ticket.new(ticket) + @local_ticket.id = ticket[:id] + @local_ticket.save + reset_primary_key_sequence('tickets') + rescue ActiveRecord::RecordNotUnique + log "Ticket #{ticket[:id]} is handled by another thead, skipping." + end + + def map(ticket) + { + title: '', + owner_id: owner_id(ticket), + customer_id: customer_id(ticket), + created_by_id: 1, + updated_by_id: 1, + } + .merge(from_mapping(ticket)) + .merge(dynamic_fields(ticket)) + end + + def dynamic_fields(ticket) + result = {} + ticket.keys.each { |key| + + key_string = key.to_s + + next if !key_string.start_with?('DynamicField_') + dynamic_field_name = key_string[13, key_string.length] + + next if Import::OTRS::DynamicFieldFactory.skip_field?( dynamic_field_name ) + dynamic_field_name = Import::OTRS::DynamicField.convert_name(dynamic_field_name) + + result[dynamic_field_name.to_sym] = ticket[key_string] + } + result + end + + def owner_id(ticket) + default = 1 + owner = ticket['Owner'] + + return default if !owner + user = user_lookup(owner) + + return user.id if user + default + end + + def customer_id(ticket) + default = 1 + customer = ticket['CustomerUserID'] + + return default if !customer + user = user_lookup(customer) + + return user.id if user + + first_customer_id = first_customer_id(ticket['Articles']) + + return first_customer_id if first_customer_id + + default + end + + def user_lookup(login) + ::User.find_by(login: login.downcase) + end + + def first_customer_id(articles) + user_id = nil + articles.each { |article| + next if article['sender'] != 'customer' + next if article['from'].empty? + + user_id = article['created_by_id'].to_i + break + } + user_id + end + + # cleanup invalid values + def fix(ticket) + utf8_encode(ticket) + fix_timestamps(ticket) + fix_close_time(ticket) + end + + def fix_timestamps(ticket) + ticket.each { |key, value| + next if value != '0000-00-00 00:00:00' + ticket[key] = nil + } + end + + # fix OTRS 3.1 bug, no close time if ticket is created + def fix_close_time(ticket) + return if ticket['StateType'] != 'closed' + return if ticket['Closed'] + return if !ticket['Closed'].empty? + ticket['Closed'] = ticket['Created'] + end + end + end +end diff --git a/lib/import/otrs/ticket_factory.rb b/lib/import/otrs/ticket_factory.rb new file mode 100644 index 000000000..501b3c594 --- /dev/null +++ b/lib/import/otrs/ticket_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module TicketFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/user.rb b/lib/import/otrs/user.rb new file mode 100644 index 000000000..007ed1030 --- /dev/null +++ b/lib/import/otrs/user.rb @@ -0,0 +1,168 @@ +module Import + module OTRS + class User + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + UserID: :id, + Comment: :note, + UserEmail: :email, + UserFirstname: :firstname, + UserLastname: :lastname, + UserLogin: :login, + UserPw: :password, + }.freeze + + def initialize(user) + import(user) + end + + private + + def import(user) + create_or_update(map(user)) + end + + def create_or_update(user) + ensure_unique_login(user) + return if updated?(user) + create(user) + end + + def updated?(user) + @local_user = ::User.find_by(id: user[:id]) + return false if !@local_user + + # only update roles if different (reduce sql statements) + if @local_user.role_ids == user[:role_ids] + user.delete(:role_ids) + end + + log "update User.find_by(id: #{user[:id]})" + @local_user.update_attributes(user) + true + end + + def create(user) + log "add User.find_by(id: #{user[:id]})" + @local_user = ::User.new(user) + @local_user.id = user[:id] + @local_user.save + reset_primary_key_sequence('users') + end + + def ensure_unique_login(user) + user[:login] = unique_login(user) + end + + def unique_login(user) + login = user[:login] + return login if ::User.where('login = ? AND id != ?', login.downcase, user[:id]).count.zero? + "#{login}_#{user[:id]}" + end + + def map(user) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(user), + source: 'OTRS Import', + role_ids: role_ids(user), + group_ids: group_ids(user), + password: password(user), + } + .merge(from_mapping(user)) + end + + def password(user) + return if !user[:password] + "{sha2}#{user[:password]}" + end + + def group_ids(user) + result = [] + queues = Import::OTRS::Requester.load('Queue') + queues.each { |queue| + + permissions = user['GroupIDs'][ queue['GroupID'] ] + + next if !permissions + next if !permissions.include?('rw') + + result.push queue['QueueID'] + } + + # lookup by roles + + # roles of user + # groups of roles + # queues of group + + result + end + + def role_ids(user) + local_role_ids = [] + roles(user).each { |role| + role_lookup = Role.lookup(name: role) + next if !role_lookup + local_role_ids.push role_lookup.id + } + local_role_ids + end + + def roles(user) + local_roles = ['Agent'] + local_roles += groups_from_otrs_groups(user) + local_roles += groups_from_otrs_roles(user) + local_roles.uniq + end + + def groups_from_otrs_groups(user) + groups = Import::OTRS::Requester.load('Group') + groups_from_groups(user, groups) + end + + def groups_from_groups(user, groups) + result = [] + groups.each { |group| + result += groups_from_otrs_group(user, group) + } + result + end + + def groups_from_otrs_group(user, group) + result = [] + return result if user['GroupIDs'].empty? + permissions = user['GroupIDs'][ group['ID'] ] + + return result if !permissions + + if group['Name'] == 'admin' && permissions.include?('rw') + result.push 'Admin' + end + + return result if group['Name'] !~ /^(stats|report)/ + return result if !( permissions.include?('ro') || permissions.include?('rw') ) + + result.push 'Report' + result + end + + def groups_from_otrs_roles(user) + result = [] + roles = Import::OTRS::Requester.load('Role') + roles.each { |role| + next if !user['RoleIDs'].include?(role['ID']) + result += groups_from_groups(user, role['GroupIDs']) + } + result + end + end + end +end diff --git a/lib/import/otrs/user_factory.rb b/lib/import/otrs/user_factory.rb new file mode 100644 index 000000000..ea49ccc77 --- /dev/null +++ b/lib/import/otrs/user_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module UserFactory + extend Import::Factory + end + end +end diff --git a/lib/import/transaction_factory.rb b/lib/import/transaction_factory.rb new file mode 100644 index 000000000..e35771a9a --- /dev/null +++ b/lib/import/transaction_factory.rb @@ -0,0 +1,18 @@ +module Import + module TransactionFactory + include Import::BaseFactory + + # rubocop:disable Style/ModuleFunction + extend self + + 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 + end + end + end +end diff --git a/lib/import/zendesk.rb b/lib/import/zendesk.rb index 838525e20..815df8146 100644 --- a/lib/import/zendesk.rb +++ b/lib/import/zendesk.rb @@ -5,7 +5,8 @@ module Import end module Import::Zendesk - module_function + # rubocop:disable Style/ModuleFunction + extend self def start Rails.logger.info 'Start import...' diff --git a/spec/fixtures/import/otrs/article/attachment/default.json b/spec/fixtures/import/otrs/article/attachment/default.json new file mode 100644 index 000000000..ee5f39b98 --- /dev/null +++ b/spec/fixtures/import/otrs/article/attachment/default.json @@ -0,0 +1,10 @@ +{ + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "201 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "201", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+dGVzdCAjMzwvYm9keT48L2h0bWw+\n" +} diff --git a/spec/fixtures/import/otrs/article/customer_phone.json b/spec/fixtures/import/otrs/article/customer_phone.json new file mode 100644 index 000000000..7e1636a6c --- /dev/null +++ b/spec/fixtures/import/otrs/article/customer_phone.json @@ -0,0 +1,97 @@ +{ + "Age": 63188310, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "test #3", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63188309, + "TicketFreeKey11": null, + "ArticleID": "3970", + "Created": "2014-11-21 00:17:41", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "5", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525461", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "customer", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "", + "Priority": "3 normal", + "To": "Postmaster", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "Postmaster", + "State": "closed successful", + "SenderTypeID": "3", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "phone", + "StateType": "closed", + "FromRealname": "Betreuter Kunde", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "\"Betreuter Kunde\" ," +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/article/customer_phone_attachment.json b/spec/fixtures/import/otrs/article/customer_phone_attachment.json new file mode 100644 index 000000000..aa1f4ef34 --- /dev/null +++ b/spec/fixtures/import/otrs/article/customer_phone_attachment.json @@ -0,0 +1,108 @@ +{ + "Age": 63188310, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "test #3", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63188309, + "TicketFreeKey11": null, + "ArticleID": "3970", + "Created": "2014-11-21 00:17:41", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "5", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525461", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "201 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "201", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+dGVzdCAjMzwvYm9keT48L2h0bWw+\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "customer", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "", + "Priority": "3 normal", + "To": "Postmaster", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "Postmaster", + "State": "closed successful", + "SenderTypeID": "3", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "phone", + "StateType": "closed", + "FromRealname": "Betreuter Kunde", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "\"Betreuter Kunde\" ," +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/customer/default.json b/spec/fixtures/import/otrs/customer/default.json new file mode 100644 index 000000000..68d9a2858 --- /dev/null +++ b/spec/fixtures/import/otrs/customer/default.json @@ -0,0 +1,130 @@ +{ + "ChangeTime": "2014-06-06 12:41:03", + "ChangeBy": "1", + "ValidID": "2", + "CustomerCompanyCity": "test922896", + "CreateTime": "2014-06-06 12:41:03", + "CustomerCompanyURL": "test922896", + "Config": { + "CustomerCompanySearchFields": [ + "customer_id", + "name" + ], + "CustomerCompanyListFields": [ + "customer_id", + "name" + ], + "Module": "Kernel::System::CustomerCompany::DB", + "CustomerCompanyKey": "customer_id", + "CustomerCompanySearchSuffix": "*", + "CacheTTL": 86400, + "CustomerCompanySearchListLimit": 250, + "CustomerCompanySearchPrefix": "", + "CustomerCompanyValid": "valid_id", + "Params": { + "Table": "customer_company", + "CaseSensitive": 0 + }, + "Map": [ + [ + "CustomerID", + "CustomerID", + "customer_id", + 0, + 1, + "var", + "", + 0 + ], + [ + "CustomerCompanyName", + "Customer", + "name", + 1, + 1, + "var", + "", + 0 + ], + [ + "CustomerCompanyStreet", + "Street", + "street", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyZIP", + "Zip", + "zip", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyCity", + "City", + "city", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyCountry", + "Country", + "country", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyURL", + "URL", + "url", + 1, + 0, + "var", + "[% Data.CustomerCompanyURL | html %]", + 0 + ], + [ + "CustomerCompanyComment", + "Comment", + "comments", + 1, + 0, + "var", + "", + 0 + ], + [ + "ValidID", + "Valid", + "valid_id", + 0, + 1, + "int", + "", + 0 + ] + ], + "Name": "Database Backend" + }, + "CustomerCompanyName": "test922896", + "CustomerCompanyCountry": "test922896", + "CustomerID": "test922896", + "CustomerCompanyStreet": "test922896", + "CustomerCompanyComment": "test922896", + "CustomerCompanyZIP": "test922896", + "Source": "CustomerCompany", + "CreateBy": "1" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/customer_user/default.json b/spec/fixtures/import/otrs/customer_user/default.json new file mode 100644 index 000000000..96fd0f920 --- /dev/null +++ b/spec/fixtures/import/otrs/customer_user/default.json @@ -0,0 +1,348 @@ +{ + "CustomerCompanyCity": "test712259", + "Config": { + "CustomerUserEmailUniqCheck": 1, + "CustomerUserSearchListLimit": 250, + "CustomerCompanySupport": 1, + "CustomerValid": "valid_id", + "CustomerUserSearchFields": [ + "login", + "first_name", + "last_name", + "customer_id" + ], + "CustomerUserSearchPrefix": "*", + "Params": { + "Table": "customer_user", + "CaseSensitive": 0 + }, + "CustomerUserListFields": [ + "first_name", + "last_name", + "email" + ], + "Map": [ + [ + "UserTitle", + "Title", + "title", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserFirstname", + "Firstname", + "first_name", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserLastname", + "Lastname", + "last_name", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserLogin", + "Username", + "login", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserPassword", + "Password", + "pw", + 0, + 0, + "var", + "", + 0 + ], + [ + "UserEmail", + "Email", + "email", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserCustomerID", + "CustomerID", + "customer_id", + 0, + 1, + "var", + "", + 0 + ], + [ + "UserPhone", + "Phone", + "phone", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserFax", + "Fax", + "fax", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserMobile", + "Mobile", + "mobile", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserStreet", + "Street", + "street", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserZip", + "Zip", + "zip", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserCity", + "City", + "city", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserCountry", + "Country", + "country", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserComment", + "Comment", + "comments", + 1, + 0, + "var", + "", + 0 + ], + [ + "ValidID", + "Valid", + "valid_id", + 0, + 1, + "int", + "", + 0 + ] + ], + "CustomerKey": "login", + "CustomerUserSearchSuffix": "*", + "Module": "Kernel::System::CustomerUser::DB", + "CacheTTL": 86400, + "Selections": {}, + "CustomerID": "customer_id", + "Name": "Database Backend", + "CustomerUserPostMasterSearchFields": [ + "email" + ], + "CustomerUserNameFields": [ + "title", + "first_name", + "last_name" + ] + }, + "UserCustomerID": "test712259", + "CustomerCompanyComment": "test712259", + "Source": "CustomerUser", + "UserTitle": "", + "CompanyConfig": { + "CustomerCompanySearchFields": [ + "customer_id", + "name" + ], + "CustomerCompanyListFields": [ + "customer_id", + "name" + ], + "Module": "Kernel::System::CustomerCompany::DB", + "CustomerCompanyKey": "customer_id", + "CustomerCompanySearchSuffix": "*", + "CacheTTL": 86400, + "CustomerCompanySearchListLimit": 250, + "CustomerCompanySearchPrefix": "", + "CustomerCompanyValid": "valid_id", + "Params": { + "Table": "customer_company", + "CaseSensitive": 0 + }, + "Map": [ + [ + "CustomerID", + "CustomerID", + "customer_id", + 0, + 1, + "var", + "", + 0 + ], + [ + "CustomerCompanyName", + "Customer", + "name", + 1, + 1, + "var", + "", + 0 + ], + [ + "CustomerCompanyStreet", + "Street", + "street", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyZIP", + "Zip", + "zip", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyCity", + "City", + "city", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyCountry", + "Country", + "country", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyURL", + "URL", + "url", + 1, + 0, + "var", + "[% Data.CustomerCompanyURL | html %]", + 0 + ], + [ + "CustomerCompanyComment", + "Comment", + "comments", + 1, + 0, + "var", + "", + 0 + ], + [ + "ValidID", + "Valid", + "valid_id", + 0, + 1, + "int", + "", + 0 + ] + ], + "Name": "Database Backend" + }, + "UserZip": null, + "UserLastname": "test669673", + "ChangeBy": "1", + "CreateTime": "2014-06-07 02:31:31", + "UserLogin": "test669673", + "UserPhone": null, + "CustomerID": "test712259", + "CustomerCompanyValidID": "1", + "CustomerCompanyZIP": "test712259", + "UserCountry": null, + "UserPassword": "f8be19af2f25837a31eff9131b0e47a5173290652c04a48b49b86474d48825ee", + "ValidID": "1", + "UserRefreshTime": "0", + "UserEmail": "qa100@t-online.de", + "UserComment": "", + "UserID": "test669673", + "UserFirstname": "test669673", + "CustomerCompanyCountry": "test712259", + "UserFax": null, + "CreateBy": "1", + "ChangeTime": "2014-06-07 02:31:31", + "UserShowTickets": "25", + "UserStreet": null, + "CustomerCompanyURL": "test712259", + "CustomerCompanyName": "test712259", + "UserMobile": null, + "CustomerCompanyStreet": "test712259", + "UserCity": null +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/checkbox/default.json b/spec/fixtures/import/otrs/dynamic_field/checkbox/default.json new file mode 100644 index 000000000..69484d471 --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/checkbox/default.json @@ -0,0 +1,15 @@ +{ + "ID": "47", + "ChangeTime": "2016-05-25 11:14:05", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2016-05-25 11:14:05", + "Label": "Checkbox Example", + "FieldOrder": "26", + "Config": { + "DefaultValue": "1" + }, + "FieldType": "Checkbox", + "Name": "CheckboxExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/date/default.json b/spec/fixtures/import/otrs/dynamic_field/date/default.json new file mode 100644 index 000000000..9c3e7ab1b --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/date/default.json @@ -0,0 +1,19 @@ +{ + "ID": "46", + "ChangeTime": "2016-05-25 11:14:06", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2014-11-23 20:01:32", + "Label": "Date Example", + "FieldOrder": "40", + "Config": { + "YearsPeriod": "0", + "YearsInFuture": "0", + "DefaultValue": "0", + "YearsInPast": "0", + "Link": "" + }, + "FieldType": "Date", + "Name": "DateExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/date_time/default.json b/spec/fixtures/import/otrs/dynamic_field/date_time/default.json new file mode 100644 index 000000000..4e2a4c3df --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/date_time/default.json @@ -0,0 +1,19 @@ +{ + "ID": "16", + "ChangeTime": "2014-09-12 09:31:58", + "InternalField": "1", + "ValidID": "1", + "CreateTime": "2014-06-26 09:53:21", + "Label": "DateTime Example", + "FieldOrder": "16", + "Config": { + "YearsPeriod": "1", + "YearsInFuture": "1", + "DefaultValue": "259200", + "YearsInPast": "9", + "Link": "" + }, + "FieldType": "DateTime", + "Name": "DateTimeExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/dropdown/default.json b/spec/fixtures/import/otrs/dynamic_field/dropdown/default.json new file mode 100644 index 000000000..2040d07f8 --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/dropdown/default.json @@ -0,0 +1,25 @@ +{ + "ID": "40", + "ChangeTime": "2016-05-25 11:14:06", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2014-08-21 14:54:15", + "Label": "Dropdown Example", + "FieldOrder": "30", + "Config": { + "TranslatableValues": "0", + "PossibleValues": { + "Hamburg": "Hamburg", + "München": "München", + "Köln": "Köln", + "Berlin": "Berlin" + }, + "TreeView": "0", + "DefaultValue": "", + "Link": "", + "PossibleNone": "1" + }, + "FieldType": "Dropdown", + "Name": "DropdownExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/multiselect/default.json b/spec/fixtures/import/otrs/dynamic_field/multiselect/default.json new file mode 100644 index 000000000..4bb4dd2cb --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/multiselect/default.json @@ -0,0 +1,24 @@ +{ + "ID": "4", + "ChangeTime": "2014-09-12 09:31:58", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2014-05-29 20:11:41", + "Label": "Multiselec tExample", + "FieldOrder": "4", + "Config": { + "TranslatableValues": "0", + "PossibleValues": { + "Hamburg": "Hamburg", + "München": "München", + "Köln": "Köln", + "Berlin": "Berlin" + }, + "TreeView": "0", + "DefaultValue": "", + "PossibleNone": "0" + }, + "FieldType": "Multiselect", + "Name": "MultiselectExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/text/default.json b/spec/fixtures/import/otrs/dynamic_field/text/default.json new file mode 100644 index 000000000..90c186bc1 --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/text/default.json @@ -0,0 +1,16 @@ +{ + "ID": "8", + "ChangeTime": "2014-09-12 09:31:58", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2014-06-19 12:11:23", + "Label": "Text Example", + "FieldOrder": "8", + "Config": { + "DefaultValue": "", + "Link": "" + }, + "FieldType": "Text", + "Name": "TextExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/text_area/default.json b/spec/fixtures/import/otrs/dynamic_field/text_area/default.json new file mode 100644 index 000000000..1322f4448 --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/text_area/default.json @@ -0,0 +1,17 @@ +{ + "ID": "8", + "ChangeTime": "2014-09-12 09:31:58", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2014-06-19 12:11:23", + "Label": "TextArea Example", + "FieldOrder": "8", + "Config": { + "DefaultValue": "", + "Rows": "20", + "Cols": "10" + }, + "FieldType": "TextArea", + "Name": "TextAreaExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/history/article/default.json b/spec/fixtures/import/otrs/history/article/default.json new file mode 100644 index 000000000..ad364f560 --- /dev/null +++ b/spec/fixtures/import/otrs/history/article/default.json @@ -0,0 +1,15 @@ +{ + "HistoryID": "11307", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "AddNote", + "TypeID": "1", + "HistoryTypeID": "15", + "ArticleID": "3973", + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%Close" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/history/move/default.json b/spec/fixtures/import/otrs/history/move/default.json new file mode 100644 index 000000000..cf9296fdb --- /dev/null +++ b/spec/fixtures/import/otrs/history/move/default.json @@ -0,0 +1,15 @@ +{ + "HistoryID": "238", + "PriorityID": "3", + "CreateTime": "2014-05-12 13:42:41", + "OwnerID": "1", + "QueueID": "5", + "HistoryType": "Move", + "TypeID": "1", + "HistoryTypeID": "16", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "1", + "TicketID": "39", + "Name": "%%Source%%5%%Target%%2" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/history/new_ticket/default.json b/spec/fixtures/import/otrs/history/new_ticket/default.json new file mode 100644 index 000000000..64fb3a8d9 --- /dev/null +++ b/spec/fixtures/import/otrs/history/new_ticket/default.json @@ -0,0 +1,15 @@ +{ + "HistoryID": "11291", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "NewTicket", + "TypeID": "1", + "HistoryTypeID": "1", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%20141121305000012%%Postmaster%%3 normal%%open%%730" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/history/priority_update/default.json b/spec/fixtures/import/otrs/history/priority_update/default.json new file mode 100644 index 000000000..b8bd76cf4 --- /dev/null +++ b/spec/fixtures/import/otrs/history/priority_update/default.json @@ -0,0 +1,15 @@ +{ + "HistoryID": "11131", + "PriorityID": "4", + "CreateTime": "2014-09-22 16:44:55", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "PriorityUpdate", + "TypeID": "2", + "HistoryTypeID": "22", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "1", + "TicketID": "721", + "Name": "%%2 low%%2%%4 high%%4" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/history/state_update/default.json b/spec/fixtures/import/otrs/history/state_update/default.json new file mode 100644 index 000000000..e75c368be --- /dev/null +++ b/spec/fixtures/import/otrs/history/state_update/default.json @@ -0,0 +1,15 @@ +{ + "HistoryID": "11305", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "StateUpdate", + "TypeID": "1", + "HistoryTypeID": "27", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%new%%open%%" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/priority/default.json b/spec/fixtures/import/otrs/priority/default.json new file mode 100644 index 000000000..25db5ea26 --- /dev/null +++ b/spec/fixtures/import/otrs/priority/default.json @@ -0,0 +1,9 @@ +{ + "ChangeBy": "1", + "ChangeTime": "2014-04-28 10:53:18", + "ID": "4", + "ValidID": "1", + "CreateTime": "2014-04-28 10:53:18", + "CreateBy": "1", + "Name": "4 high" +} diff --git a/spec/fixtures/import/otrs/queue/default.json b/spec/fixtures/import/otrs/queue/default.json new file mode 100644 index 000000000..4db1dd2f8 --- /dev/null +++ b/spec/fixtures/import/otrs/queue/default.json @@ -0,0 +1,25 @@ +{ + "ValidID": "2", + "FollowUpLock": "0", + "RealName": "UnitTest49130", + "QueueID": "11", + "FirstResponseNotify": "0", + "UpdateTime": "0", + "Email": "unittest15486@example.com", + "ChangeTime": "2014-05-13 10:54:11", + "UnlockTimeout": "0", + "Calendar": "", + "CreateTime": "2014-05-13 10:54:11", + "Comment": "Some comment", + "UpdateNotify": "0", + "DefaultSignKey": "", + "GroupID": "1", + "SolutionTime": "0", + "SolutionNotify": "0", + "SystemAddressID": "8", + "FollowUpID": "1", + "SalutationID": "1", + "Name": "UnitTestQueue45699", + "SignatureID": "8", + "FirstResponseTime": "0" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/state/default.json b/spec/fixtures/import/otrs/state/default.json new file mode 100644 index 000000000..13c8a8704 --- /dev/null +++ b/spec/fixtures/import/otrs/state/default.json @@ -0,0 +1,10 @@ +{ + "ChangeTime": "2014-04-28 10:53:18", + "ID": "2", + "ValidID": "1", + "TypeID": "3", + "CreateTime": "2014-04-28 10:53:18", + "TypeName": "closed", + "Comment": "Ticket is closed successful.", + "Name": "closed successful" +} diff --git a/spec/fixtures/import/otrs/ticket/default.json b/spec/fixtures/import/otrs/ticket/default.json new file mode 100644 index 000000000..a347cbd61 --- /dev/null +++ b/spec/fixtures/import/otrs/ticket/default.json @@ -0,0 +1,770 @@ +{ + "Age": 63277486, + "PriorityID": "3", + "ServiceID": "", + "DynamicField_SugarCRMCompanySelection": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "Created": "2014-11-21 00:17:40", + "DynamicField_TicketFreeText11": null, + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "QueueID": "1", + "CreateBy": "3", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "SolutionInMin": 0, + "EscalationResponseTime": "0", + "UnlockTimeout": "1416525661", + "DynamicField_ProcessManagementProcessID": null, + "ArchiveFlag": "n", + "DynamicField_CheckboxExample": null, + "SolutionTime": "2014-11-21 00:21:08", + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Type": "Incident", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "ResponsibleID": "1", + "ChangeBy": "3", + "DynamicField_Combine": null, + "RealTillTimeNotUsed": "0", + "GroupID": "1", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "Priority": "3 normal", + "DynamicField_SugarCRMCompanySelectedID": null, + "History": [ + { + "HistoryID": "11291", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "NewTicket", + "TypeID": "1", + "HistoryTypeID": "1", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%20141121305000012%%Postmaster%%3 normal%%open%%730" + }, + { + "HistoryID": "11292", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "TicketDynamicFieldUpdate", + "TypeID": "1", + "HistoryTypeID": "28", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "1", + "TicketID": "730", + "Name": "%%FieldName%%ZammadMigratorChanged%%Value%%1" + }, + { + "HistoryID": "11293", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "ServiceUpdate", + "TypeID": "1", + "HistoryTypeID": "38", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%NULL%%%%NULL%%" + }, + { + "HistoryID": "11294", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "SLAUpdate", + "TypeID": "1", + "HistoryTypeID": "39", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%NULL%%%%NULL%%" + }, + { + "HistoryID": "11295", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "CustomerUpdate", + "TypeID": "1", + "HistoryTypeID": "21", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%CustomerID=3333333333;CustomerUser=BetreuterKunde2;" + }, + { + "HistoryID": "11296", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "TicketDynamicFieldUpdate", + "TypeID": "1", + "HistoryTypeID": "28", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%FieldName%%ITSMDueDate%%Value%%2014-11-24 00:15:00" + }, + { + "HistoryID": "11297", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "PhoneCallCustomer", + "TypeID": "1", + "HistoryTypeID": "14", + "ArticleID": "3970", + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%" + }, + { + "HistoryID": "11298", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:42", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "SendAutoReply", + "TypeID": "1", + "HistoryTypeID": "4", + "ArticleID": "3971", + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%\"Betreuter Kunde\" " + }, + { + "HistoryID": "11299", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:42", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "OwnerUpdate", + "TypeID": "1", + "HistoryTypeID": "23", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%agent-2%%3" + }, + { + "HistoryID": "11300", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:43", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "SendCustomerNotification", + "TypeID": "1", + "HistoryTypeID": "10", + "ArticleID": "3972", + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%kunde2@kunde.de" + }, + { + "HistoryID": "11301", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:43", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "SendAgentNotification", + "TypeID": "1", + "HistoryTypeID": "9", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%TEST%%agent-1@example.com" + }, + { + "HistoryID": "11302", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:44", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "SendAgentNotification", + "TypeID": "1", + "HistoryTypeID": "9", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%TEST%%agent-2-for-role-2@example.com" + }, + { + "HistoryID": "11303", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:01", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "Lock", + "TypeID": "1", + "HistoryTypeID": "17", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%lock" + }, + { + "HistoryID": "11304", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:01", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "Misc", + "TypeID": "1", + "HistoryTypeID": "25", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "Reset of unlock time." + }, + { + "HistoryID": "11305", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "StateUpdate", + "TypeID": "1", + "HistoryTypeID": "27", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%open%%closed successful%%" + }, + { + "HistoryID": "11306", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "Unlock", + "TypeID": "1", + "HistoryTypeID": "18", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%unlock" + }, + { + "HistoryID": "11307", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "AddNote", + "TypeID": "1", + "HistoryTypeID": "15", + "ArticleID": "3973", + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%Close" + }, + { + "HistoryID": "11308", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:09", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "TicketDynamicFieldUpdate", + "TypeID": "1", + "HistoryTypeID": "28", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%FieldName%%ITSMReviewRequired%%Value%%No" + } + ], + "UntilTime": 0, + "EscalationUpdateTime": "0", + "DynamicField_ITSMRepairStartTime": null, + "Queue": "Postmaster", + "State": "closed successful", + "Closed": "2014-11-21 00:21:08", + "Title": "test #3", + "DynamicField_ZammadMigratorChangedOld": "1", + "DynamicField_ScomState": null, + "FirstLock": "2014-11-21 00:21:01", + "DynamicField_Department": null, + "Articles": [ + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "test #3", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277485, + "TicketFreeKey11": null, + "ArticleID": "3970", + "Created": "2014-11-21 00:17:41", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "5", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525461", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "201 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "201", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+dGVzdCAjMzwvYm9keT48L2h0bWw+\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "customer", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "", + "Priority": "3 normal", + "To": "Postmaster", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "Postmaster", + "State": "closed successful", + "SenderTypeID": "3", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "phone", + "StateType": "closed", + "FromRealname": "Betreuter Kunde", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "\"Betreuter Kunde\" ," + }, + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "This is a demo text which is send to every inquiry.\nIt could contain something like:\n\nThanks for your email. A new ticket has been created.\n\nYou wrote:\n> test #3\n\n\nYour email will be answered by a human ASAP\n\nHave fun with OTRS! :-)\n\nYour OTRS Team\n", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277484, + "TicketFreeKey11": null, + "ArticleID": "3971", + "Created": "2014-11-21 00:17:42", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "1", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525462", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "591 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "591", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+VGhpcyBpcyBhIGRlbW8gdGV4dCB3aGljaCBpcyBzZW5kIHRvIGV2ZXJ5IGlucXVp\ncnkuPGJyLz5JdCBjb3VsZCBjb250YWluIHNvbWV0aGluZyBsaWtlOjxici8+PGJyLz5UaGFua3Mg\nZm9yIHlvdXIgZW1haWwuIEEgbmV3IHRpY2tldCBoYXMgYmVlbiBjcmVhdGVkLjxici8+PGJyLz5Z\nb3Ugd3JvdGU6PGJyLz48ZGl2ICB0eXBlPSJjaXRlIiBzdHlsZT0iYm9yZGVyOm5vbmU7Ym9yZGVy\nLWxlZnQ6c29saWQgYmx1ZSAxLjVwdDtwYWRkaW5nOjBjbSAwY20gMGNtIDQuMHB0Ij50ZXN0ICMz\nPC9kaXY+PGJyLz48YnIvPllvdXIgZW1haWwgd2lsbCBiZSBhbnN3ZXJlZCBieSBhIGh1bWFuIEFT\nQVA8YnIvPjxici8+SGF2ZSBmdW4gd2l0aCBPVFJTISA6LSk8YnIvPjxici8+WW91ciBPVFJTIFRl\nYW08YnIvPjwvYm9keT48L2h0bWw+\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "system", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "[Ticket#20141121305000012] RE: test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "<1416525462.277752.866124677.730.3@yourhost.example.com>", + "Priority": "3 normal", + "To": "\"Betreuter Kunde\" ", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "Betreuter Kunde", + "State": "closed successful", + "SenderTypeID": "2", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "email-external", + "StateType": "closed", + "FromRealname": "OTRS System", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "OTRS System " + }, + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "CustomerUser: Betreuter Kunde\nCustomerUser Email: kunde2@kunde.de\nPartner Email: \"Betreuter Kunde\" ,", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277483, + "TicketFreeKey11": null, + "ArticleID": "3972", + "Created": "2014-11-21 00:17:43", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "3", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525463", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "system", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "[Ticket#20141121305000012] EventNotification: test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "<1416525463.523919.240611208.730.3@yourhost.example.com>", + "Priority": "3 normal", + "To": "kunde2@kunde.de", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "kunde2@kunde.de", + "State": "closed successful", + "SenderTypeID": "2", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "email-notification-ext", + "StateType": "closed", + "FromRealname": "OTRS System", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "OTRS System " + }, + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "close", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277278, + "TicketFreeKey11": null, + "ArticleID": "3973", + "Created": "2014-11-21 00:21:08", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "9", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525668", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "199 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "199", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+Y2xvc2U8L2JvZHk+PC9odG1sPg==\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "agent", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "Close", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "", + "Priority": "3 normal", + "To": "", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "State": "closed successful", + "SenderTypeID": "1", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "note-internal", + "StateType": "closed", + "FromRealname": "agent-2 firstname äöüß agent-2 lastname äöüß", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "\"agent-2 firstname äöüß agent-2 lastname äöüß\" " + } + ], + "StateType": "closed", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "CreateTimeUnix": "1416525460", + "Lock": "unlock", + "SLAID": "", + "DynamicField_ITSMCriticality": null +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/user/default.json b/spec/fixtures/import/otrs/user/default.json new file mode 100644 index 000000000..b13dc5baa --- /dev/null +++ b/spec/fixtures/import/otrs/user/default.json @@ -0,0 +1,135 @@ +{ + "OutOfOffice": "1", + "OutOfOfficeStartMonth": "9", + "UserStoredFilterColumns-AgentTicketLockedView": "{}", + "UserTicketOverviewSmallPageShown": "35", + "UserCreateWorkOrderNextMask": "AgentITSMWorkOrderZoom", + "OutOfOfficeEndYear": "2014", + "UserDashboardTicketGenericFilter0110-TicketEscalation": "All", + "UserDashboardPref0120-TicketNew-Columns": "{\"Columns\":{\"Changed\":0,\"CustomerID\":0,\"CustomerName\":0,\"CustomerUserID\":0,\"DynamicField_CustomerLocation\":0,\"EscalationResponseTime\":0,\"EscalationSolutionTime\":0,\"EscalationTime\":0,\"EscalationUpdateTime\":0,\"Lock\":0,\"Owner\":0,\"PendingTime\":0,\"Priority\":0,\"Responsible\":0,\"SLA\":0,\"State\":0,\"Type\":0,\"Age\":1,\"Title\":1,\"Queue\":1,\"Service\":1,\"TicketNumber\":1},\"Order\":[\"Age\",\"Title\",\"Queue\",\"Service\",\"TicketNumber\"]}", + "UserLastUsedZoomViewType": "", + "OutOfOfficeStartDay": "10", + "UserStoredFilterColumns-AgentTicketStatusView": "{}", + "UserTitle": null, + "UserLastname": "OTRS", + "UserTicketOverviewMediumPageShown": "20", + "OutOfOfficeEndDay": "12", + "CreateTime": "2014-04-28 10:53:18", + "UserTicketOverviewPreviewPageShown": "15", + "UserLogin": "root@localhost", + "UserFilterColumnsEnabled-AgentTicketEscalationView": "[\"TicketNumber\",\"Age\",\"EscalationTime\",\"EscalationResponseTime\",\"EscalationSolutionTime\",\"EscalationUpdateTime\",\"Title\",\"State\",\"Lock\",\"Queue\",\"Owner\",\"CustomerID\"]", + "UserLanguage": "de", + "UserDashboardPref0110-TicketEscalation-Columns": "{\"Columns\":{\"Changed\":0,\"CustomerID\":0,\"CustomerUserID\":0,\"EscalationResponseTime\":0,\"EscalationSolutionTime\":0,\"EscalationTime\":0,\"EscalationUpdateTime\":0,\"Lock\":0,\"Owner\":0,\"PendingTime\":0,\"Priority\":0,\"Queue\":0,\"Responsible\":0,\"SLA\":0,\"Service\":0,\"State\":0,\"Type\":0,\"Age\":1,\"Title\":1,\"CustomerName\":1,\"TicketNumber\":1},\"Order\":[\"Age\",\"Title\",\"CustomerName\",\"TicketNumber\"]}", + "OutOfOfficeStartYear": "2014", + "UserDashboardPref0120-TicketNew-Shown": "10", + "UserFullname": "Admin OTRS", + "UserLastLoginTimestamp": "2016-08-10 19:37:44", + "UserLastLogin": "1470850664", + "UserMarkTicketUnseenRedirectURL": "Action=AgentTicketZoom;TicketID=###TicketID####1", + "AdminDynamicFieldsOverviewPageShown": "35", + "UserChangeOverviewSmallPageShown": "25", + "RoleIDs": [], + "ValidID": "1", + "UserStoredFilterColumns-AgentTicketQueue": "{}", + "UserEmail": "root@localhost", + "UserRefreshTime": "0", + "UserDashboardPref0130-TicketOpen-Shown": "10", + "UserTicketOverviewAgentTicketQueue": "Small", + "UserID": "1", + "UserDashboardTicketGenericColumnFiltersRealKeys0120-TicketNew": "{\"QueueIDs\":[\"1\"]}", + "wpt22": "1", + "UserMarkTicketSeenRedirectURL": "Action=AgentTicketZoom;TicketID=###TicketID####1", + "UserStoredFilterColumns-AgentTicketEscalationView": "{}", + "UserDashboardTicketGenericFilter0120-TicketNew": "MyQueues", + "UserCreateNextMask": "", + "UserFirstname": "Admin", + "UserPw": "9faaba2ab242a99bbb6992e9424386375f6757c17e6484ae570f39d9cad9f28ea", + "UserDashboardPref0110-TicketEscalation-Shown": "10", + "UserFilterColumnsEnabled-AgentTicketQueue": "[\"TicketNumber\",\"Age\",\"Title\",\"State\",\"Lock\",\"DynamicField_CustomerLocation\",\"Queue\",\"Owner\",\"CustomerID\",\"DynamicField_Hostname\"]", + "OutOfOfficeEndMonth": "9", + "ChangeTime": "2014-04-28 10:53:18", + "UserDashboardPref0130-TicketOpen-Columns": "{\"Columns\":{\"Changed\":0,\"CustomerID\":0,\"CustomerUserID\":0,\"EscalationResponseTime\":0,\"EscalationTime\":0,\"EscalationUpdateTime\":0,\"Lock\":0,\"Owner\":0,\"PendingTime\":0,\"Priority\":0,\"Queue\":0,\"Responsible\":0,\"SLA\":0,\"Service\":0,\"State\":0,\"Type\":0,\"Age\":1,\"DynamicField_CustomerLocation\":1,\"Title\":1,\"CustomerName\":1,\"EscalationSolutionTime\":1,\"TicketNumber\":1},\"Order\":[\"Age\",\"DynamicField_CustomerLocation\",\"Title\",\"CustomerName\",\"EscalationSolutionTime\",\"TicketNumber\"]}", + "UserTicketOverviewAgentTicketSearch": "Small", + "UserTicketOverviewAgentCustomerSearch": "Small", + "UserDashboardTicketGenericColumnFilters0120-TicketNew": "{\"Queue\":\"1\"}", + "GroupIDs": { + "6": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "3": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "7": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "2": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "8": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "1": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "4": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "5": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ] + }, + "UserConfigItemOverviewSmallPageShown": "25", + "UserAuthBackend": "", + "UserTicketOverviewAgentTicketLockedView": "Small", + "UserTicketOverviewAgentTicketEscalationView": "Small", + "UserTicketOverviewAgentTicketStatusView": "Small", + "UserLoginFailed": "0" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/user/no_groups.json b/spec/fixtures/import/otrs/user/no_groups.json new file mode 100644 index 000000000..24022d981 --- /dev/null +++ b/spec/fixtures/import/otrs/user/no_groups.json @@ -0,0 +1,36 @@ +{ + "OutOfOffice": "0", + "OutOfOfficeStartMonth": "11", + "UserTicketOverviewSmallPageShown": "25", + "UserCreateWorkOrderNextMask": "AgentITSMWorkOrderZoom", + "OutOfOfficeEndYear": "2014", + "OutOfOfficeStartDay": "14", + "UserTitle": "", + "UserLastname": "agent-2-for-role-2", + "UserTicketOverviewMediumPageShown": "20", + "OutOfOfficeEndDay": "15", + "CreateTime": "2014-11-14 00:53:20", + "UserTicketOverviewPreviewPageShown": "15", + "UserLogin": "agent-2-for-role-2", + "UserLanguage": "en", + "OutOfOfficeStartYear": "2014", + "UserFullname": "agent-2-for-role-2 agent-2-for-role-2", + "UserChangeOverviewSmallPageShown": "25", + "AdminDynamicFieldsOverviewPageShown": "25", + "RoleIDs": [ + "3" + ], + "ValidID": "1", + "UserRefreshTime": "0", + "UserComment": "", + "UserEmail": "agent-2-for-role-2@example.com", + "UserID": "6", + "UserCreateNextMask": "", + "UserFirstname": "agent-2-for-role-2", + "UserPw": "9edb001ad7900daea0622d89225c9ca729749fd12ae5ea044f072d1b7c56c8cc", + "OutOfOfficeEndMonth": "11", + "ChangeTime": "2014-11-14 00:53:20", + "GroupIDs": {}, + "UserSkin": "default", + "UserConfigItemOverviewSmallPageShown": "25" +} \ No newline at end of file diff --git a/spec/import/base_factory_examples.rb b/spec/import/base_factory_examples.rb new file mode 100644 index 000000000..ca15fafcd --- /dev/null +++ b/spec/import/base_factory_examples.rb @@ -0,0 +1,24 @@ +require 'import/import_factory_examples' + +RSpec.shared_examples 'Import::BaseFactory' do + it_behaves_like 'Import factory' + + it 'responds to pre_import_hook' do + expect(described_class).to respond_to('pre_import_hook') + end + it 'responds to backend_class' do + expect(described_class).to respond_to('backend_class') + end + it 'responds to skip?' do + expect(described_class).to respond_to('skip?') + end +end + +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) + described_class.import([record]) + end +end diff --git a/spec/import/base_factory_spec.rb b/spec/import/base_factory_spec.rb new file mode 100644 index 000000000..39462d826 --- /dev/null +++ b/spec/import/base_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::Factory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/factory_examples.rb b/spec/import/factory_examples.rb new file mode 100644 index 000000000..ef43fb1a9 --- /dev/null +++ b/spec/import/factory_examples.rb @@ -0,0 +1,5 @@ +require 'import/base_factory_examples' + +RSpec.shared_examples 'Import::Factory' do + it_behaves_like 'Import::BaseFactory' +end diff --git a/spec/import/factory_spec.rb b/spec/import/factory_spec.rb new file mode 100644 index 000000000..397a035bd --- /dev/null +++ b/spec/import/factory_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::Factory do + it_behaves_like 'Import::Factory' + it_behaves_like 'Import::BaseFactory extender' +end diff --git a/spec/import/helper_examples.rb b/spec/import/helper_examples.rb new file mode 100644 index 000000000..6238fbb6a --- /dev/null +++ b/spec/import/helper_examples.rb @@ -0,0 +1,18 @@ +RSpec.shared_examples 'Import::Helper' do + + it 'responds to check_import_mode' do + expect(described_class).to respond_to('check_import_mode') + end + + it 'responds to log' do + expect(described_class).to respond_to('log') + end + + it 'responds to utf8_encode' do + expect(described_class).to respond_to('utf8_encode') + end + + it 'responds to reset_primary_key_sequence' do + expect(described_class).to respond_to('reset_primary_key_sequence') + end +end diff --git a/spec/import/helper_spec.rb b/spec/import/helper_spec.rb new file mode 100644 index 000000000..4194c3470 --- /dev/null +++ b/spec/import/helper_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' +require 'import/helper_examples' + +RSpec.describe Import::Helper do + it_behaves_like 'Import::Helper' + + it 'checks if import_mode is active' do + expect(Setting).to receive(:get).with('import_mode').and_return(true) + expect( described_class.check_import_mode ).to be true + end + + it 'throws an exception if import_mode is disabled' do + expect(Setting).to receive(:get).with('import_mode').and_return(false) + expect { described_class.check_import_mode }.to raise_error(RuntimeError) + end +end diff --git a/spec/import/import_factory_examples.rb b/spec/import/import_factory_examples.rb new file mode 100644 index 000000000..258f8d433 --- /dev/null +++ b/spec/import/import_factory_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'Import factory' do + it 'responds to import' do + expect(described_class).to respond_to('import') + end +end diff --git a/spec/import/importer_examples.rb b/spec/import/importer_examples.rb new file mode 100644 index 000000000..2bd0b87c9 --- /dev/null +++ b/spec/import/importer_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'Import backend' do + it 'responds to start' do + expect(described_class).to respond_to('start') + end +end diff --git a/spec/import/otrs/article/attachment_factory_spec.rb b/spec/import/otrs/article/attachment_factory_spec.rb new file mode 100644 index 000000000..1d5047685 --- /dev/null +++ b/spec/import/otrs/article/attachment_factory_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' +require 'import/import_factory_examples' + +RSpec.describe Import::OTRS::Article::AttachmentFactory do + it_behaves_like 'Import factory' + + def load_attachment_json(file) + json_fixture("import/otrs/article/attachment/#{file}") + end + + let(:local_article) { instance_double(Ticket::Article, ticket_id: 1337, id: 42) } + let(:attachments) { + [ + load_attachment_json('default'), + load_attachment_json('default'), + load_attachment_json('default') + ] + } + let(:start_import) { + described_class.import( + attachments: attachments, + local_article: local_article + ) + } + + def import_expectations + expect(Store).to receive(:add).exactly(3).times.with(hash_including( + object: 'Ticket::Article', + o_id: local_article.id, + )) + end + + def article_attachment_expectations(article_attachments) + expect(local_article).to receive(:attachments).and_return(article_attachments) + end + + it 'imports' do + article_attachment_expectations([]) + import_expectations + start_import + end + + it 'deletes old and reimports' do + dummy_attachment = double() + expect(dummy_attachment).to receive(:delete) + article_attachment_expectations([dummy_attachment]) + import_expectations + start_import + end + + it 'skips import for same count' do + article_attachment_expectations([1, 2, 3]) + expect(Store).not_to receive(:add) + start_import + end +end diff --git a/spec/import/otrs/article_customer_factory_spec.rb b/spec/import/otrs/article_customer_factory_spec.rb new file mode 100644 index 000000000..69cc180b8 --- /dev/null +++ b/spec/import/otrs/article_customer_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::ArticleCustomerFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/article_customer_sepc.rb b/spec/import/otrs/article_customer_sepc.rb new file mode 100644 index 000000000..45396124a --- /dev/null +++ b/spec/import/otrs/article_customer_sepc.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::ArticleCustomer do + + def load_article_json(file) + json_fixture("import/otrs/article/#{file}") + end + + let(:instance_id) { 1337 } + let(:existing_object) { instance_double(import_object) } + let(:import_object) { User } + let(:object_structure) { load_article_json('customer_phone') } + let(:start_import_test) { described_class.new(object_structure) } + + it 'finds customers by email' do + expect(import_object).to receive(:find_by).with(email: 'kunde2@kunde.de').and_return(existing_object) + expect(existing_object).to receive(:id).and_return(instance_id) + expect(import_object).not_to receive(:create) + start_import_test + expect(object_structure['created_by_id']).to eq(instance_id) + end + + it 'finds customers by login' do + expect(import_object).to receive(:find_by).with(email: 'kunde2@kunde.de') + expect(import_object).to receive(:find_by).with(login: 'kunde2@kunde.de').and_return(existing_object) + expect(existing_object).to receive(:id).and_return(instance_id) + expect(import_object).not_to receive(:create) + start_import_test + expect(object_structure['created_by_id']).to eq(instance_id) + end + + it 'creates customers' do + expect(import_object).to receive(:find_by).at_least(:once) + expect(import_object).to receive(:create).and_return(existing_object) + expect(existing_object).to receive(:id).and_return(instance_id) + start_import_test + expect(object_structure['created_by_id']).to eq(instance_id) + end +end diff --git a/spec/import/otrs/article_factory_spec.rb b/spec/import/otrs/article_factory_spec.rb new file mode 100644 index 000000000..f6a943228 --- /dev/null +++ b/spec/import/otrs/article_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::ArticleFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/article_spec.rb b/spec/import/otrs/article_spec.rb new file mode 100644 index 000000000..2fa9cb39e --- /dev/null +++ b/spec/import/otrs/article_spec.rb @@ -0,0 +1,79 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Article do + + def creates_with(zammad_structure) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_article_json(file) + json_fixture("import/otrs/article/#{file}") + end + + let(:import_object) { Ticket::Article } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'customer phone' do + + let(:object_structure) { load_article_json('customer_phone_attachment') } + + it 'creates' do + zammad_structure = { + created_by_id: '3', + updated_by_id: 1, + ticket_id: '730', + id: '3970', + body: 'test #3', + from: '"Betreuter Kunde" ,', + to: 'Postmaster', + cc: '', + subject: 'test #3', + in_reply_to: '', + message_id: '', + references: '', + updated_at: '2014-11-21 00:21:08', + created_at: '2014-11-21 00:17:41', + type_id: 5, + internal: false, + sender_id: 2 + } + expect(Import::OTRS::Article::AttachmentFactory).to receive(:import) + creates_with(zammad_structure) + end + + it 'updates' do + zammad_structure = { + created_by_id: '3', + updated_by_id: 1, + ticket_id: '730', + id: '3970', + body: 'test #3', + from: '"Betreuter Kunde" ,', + to: 'Postmaster', + cc: '', + subject: 'test #3', + in_reply_to: '', + message_id: '', + references: '', + updated_at: '2014-11-21 00:21:08', + created_at: '2014-11-21 00:17:41', + type_id: 5, + internal: false, + sender_id: 2 + } + expect(Import::OTRS::Article::AttachmentFactory).to receive(:import) + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/async_examples.rb b/spec/import/otrs/async_examples.rb new file mode 100644 index 000000000..4ae347102 --- /dev/null +++ b/spec/import/otrs/async_examples.rb @@ -0,0 +1,9 @@ +RSpec.shared_examples 'Import::OTRS::Async' do + it 'responds to start_bg' do + expect(described_class).to respond_to('start_bg') + end + + it 'responds to status_bg' do + expect(described_class).to respond_to('status_bg') + end +end diff --git a/spec/import/otrs/customer_factory_spec.rb b/spec/import/otrs/customer_factory_spec.rb new file mode 100644 index 000000000..40298e1c8 --- /dev/null +++ b/spec/import/otrs/customer_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::CustomerFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/customer_spec.rb b/spec/import/otrs/customer_spec.rb new file mode 100644 index 000000000..ca16752f7 --- /dev/null +++ b/spec/import/otrs/customer_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Customer do + + def creates_with(zammad_structure) + expect(import_object).to receive(:create).with(zammad_structure).and_return(existing_object) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_customer_json(file) + json_fixture("import/otrs/customer/#{file}") + end + + let(:import_object) { Organization } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'Organization' do + + let(:object_structure) { load_customer_json('default') } + let(:zammad_structure) { + { + created_by_id: '1', + updated_by_id: '1', + active: false, + updated_at: '2014-06-06 12:41:03', + created_at: '2014-06-06 12:41:03', + name: 'test922896', + note: 'test922896' + }} + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end + + context 'OTRS CustomerID' do + + let(:customer_id) { 'test922896' } + let(:object_structure) { load_customer_json('default') } + let(:otrs_dummy_response) { + [ + object_structure + ] + } + + it 'responds to by_customer_id' do + expect(described_class).to respond_to('by_customer_id') + end + + it 'finds Organizations by OTRS CustomerID' do + expect(Import::OTRS::Requester).to receive(:load).and_return(otrs_dummy_response) + expect(import_object).to receive(:find_by).with(name: customer_id).and_return(existing_object) + + expect(described_class.by_customer_id(customer_id)).to be(existing_object) + end + end +end diff --git a/spec/import/otrs/customer_user_factory_spec.rb b/spec/import/otrs/customer_user_factory_spec.rb new file mode 100644 index 000000000..bb0761d52 --- /dev/null +++ b/spec/import/otrs/customer_user_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::CustomerUserFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/customer_user_spec.rb b/spec/import/otrs/customer_user_spec.rb new file mode 100644 index 000000000..c208a7935 --- /dev/null +++ b/spec/import/otrs/customer_user_spec.rb @@ -0,0 +1,73 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::CustomerUser do + + def creates_with(zammad_structure) + expect_organization_lookup + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect_organization_lookup + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:role_ids).and_return([]).at_least(:once) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def expect_organization_lookup + expect(Import::OTRS::Customer).to receive(:by_customer_id).and_return(organization) + expect(organization).to receive(:id).and_return(organization_id) + end + + def load_customer_json(file) + json_fixture("import/otrs/customer_user/#{file}") + end + + let(:import_object) { User } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + let(:organization) { instance_double(Organization) } + let(:organization_id) { 1337 } + + context 'Customer User' do + + let(:object_structure) { load_customer_json('default') } + let(:zammad_structure) { + { + created_by_id: '1', + updated_by_id: '1', + active: true, + source: 'OTRS Import', + organization_id: 1337, + role_ids: [3], + updated_at: '2014-06-07 02:31:31', + created_at: '2014-06-07 02:31:31', + note: '', + email: 'qa100@t-online.de', + firstname: 'test669673', + lastname: 'test669673', + login: 'test669673', + password: 'f8be19af2f25837a31eff9131b0e47a5173290652c04a48b49b86474d48825ee', + phone: nil, + fax: nil, + mobile: nil, + street: nil, + zip: nil, + city: nil, + country: nil + }} + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/diff_examples.rb b/spec/import/otrs/diff_examples.rb new file mode 100644 index 000000000..679844d5c --- /dev/null +++ b/spec/import/otrs/diff_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'Import::OTRS::Diff' do + it 'responds to diff_worker' do + expect(described_class).to respond_to('diff_worker') + end +end diff --git a/spec/import/otrs/dynamic_field/checkbox_spec.rb b/spec/import/otrs/dynamic_field/checkbox_spec.rb new file mode 100644 index 000000000..61872b7d0 --- /dev/null +++ b/spec/import/otrs/dynamic_field/checkbox_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField::Checkbox do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS Checkbox DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'checkbox_example', + display: 'Checkbox Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '26', + created_by_id: 1, + updated_by_id: 1, + data_type: 'boolean', + data_option: { + default: true, + options: { + true => 'Yes', + false => 'No' + }, + null: false, + translate: true + } + } + + dynamic_field_from_json('checkbox/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/date_spec.rb b/spec/import/otrs/dynamic_field/date_spec.rb new file mode 100644 index 000000000..4cafe944d --- /dev/null +++ b/spec/import/otrs/dynamic_field/date_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +# this require is required (hehe) because of Rails autoloading +# which causes this error: +# warning: toplevel constant Date referenced by Import::OTRS::DynamicField::Date +# and rspec thinks we want to test Date and stores it into described_class... +require 'import/otrs/dynamic_field/date' + +RSpec.describe Import::OTRS::DynamicField::Date do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS Date DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'date_example', + display: 'Date Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '40', + created_by_id: 1, + updated_by_id: 1, + data_type: 'date', + data_option: { + future: false, + past: false, + diff: 0, + null: false + } + } + + dynamic_field_from_json('date/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/date_time_spec.rb b/spec/import/otrs/dynamic_field/date_time_spec.rb new file mode 100644 index 000000000..2e05c246b --- /dev/null +++ b/spec/import/otrs/dynamic_field/date_time_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +# this require is required (hehe) because of Rails autoloading +# which causes this error: +# warning: toplevel constant DateTime referenced by Import::OTRS::DynamicField::DateTime +# and rspec thinks we want to test Date and stores it into described_class... +require 'import/otrs/dynamic_field/date_time' + +RSpec.describe Import::OTRS::DynamicField::DateTime do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS DateTime DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'date_time_example', + display: 'DateTime Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: false, + position: '16', + created_by_id: 1, + updated_by_id: 1, + data_type: 'datetime', + data_option: { + future: true, + past: true, + diff: 72, + null: false + } + } + + dynamic_field_from_json('date_time/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/dropdown_spec.rb b/spec/import/otrs/dynamic_field/dropdown_spec.rb new file mode 100644 index 000000000..da3c1d673 --- /dev/null +++ b/spec/import/otrs/dynamic_field/dropdown_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField::Dropdown do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS Dropdown DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'dropdown_example', + display: 'Dropdown Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '30', + created_by_id: 1, + updated_by_id: 1, + data_type: 'select', + data_option: { + default: '', + multiple: false, + options: { + 'Hamburg' => 'Hamburg', + 'München' => 'München', + 'Köln' => 'Köln', + 'Berlin' => 'Berlin' + }, + null: true, + translate: false + } + } + + dynamic_field_from_json('dropdown/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/multiselect_spec.rb b/spec/import/otrs/dynamic_field/multiselect_spec.rb new file mode 100644 index 000000000..a188bdfca --- /dev/null +++ b/spec/import/otrs/dynamic_field/multiselect_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField::Multiselect do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS Multiselect DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'multiselect_example', + display: 'Multiselec tExample', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '4', + created_by_id: 1, + updated_by_id: 1, + data_type: 'select', + data_option: { + default: '', + multiple: true, + options: { + 'Hamburg' => 'Hamburg', + 'München' => 'München', + 'Köln' => 'Köln', + 'Berlin' => 'Berlin' + }, + null: false, + translate: false + } + } + + dynamic_field_from_json('multiselect/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/text_area_spec.rb b/spec/import/otrs/dynamic_field/text_area_spec.rb new file mode 100644 index 000000000..f12d541cd --- /dev/null +++ b/spec/import/otrs/dynamic_field/text_area_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField::TextArea do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS TextArea DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'text_area_example', + display: 'TextArea Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '8', + created_by_id: 1, + updated_by_id: 1, + data_type: 'textarea', + data_option: { + default: '', + rows: '20', + null: false + } + } + + dynamic_field_from_json('text_area/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/text_spec.rb b/spec/import/otrs/dynamic_field/text_spec.rb new file mode 100644 index 000000000..80486412e --- /dev/null +++ b/spec/import/otrs/dynamic_field/text_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField::Text do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS Text DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'text_example', + display: 'Text Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '8', + created_by_id: 1, + updated_by_id: 1, + data_type: 'input', + data_option: { + default: '', + type: 'text', + maxlength: 255, + null: false + } + } + + dynamic_field_from_json('text/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field_examples.rb b/spec/import/otrs/dynamic_field_examples.rb new file mode 100644 index 000000000..edab67180 --- /dev/null +++ b/spec/import/otrs/dynamic_field_examples.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +def dynamic_field_from_json(file, zammad_structure) + expect(ObjectManager::Attribute).to receive(:add).with(zammad_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + described_class.new(load_dynamic_field_json(file)) +end + +def load_dynamic_field_json(file) + json_fixture("import/otrs/dynamic_field/#{file}") +end + +RSpec.shared_examples 'Import::OTRS::DynamicField' do + it 'responds to convert_name' do + expect(described_class).to respond_to('convert_name') + end +end diff --git a/spec/import/otrs/dynamic_field_factory_spec.rb b/spec/import/otrs/dynamic_field_factory_spec.rb new file mode 100644 index 000000000..e10686298 --- /dev/null +++ b/spec/import/otrs/dynamic_field_factory_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' +require 'import/factory_examples' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicFieldFactory do + it_behaves_like 'Import::Factory' + + let(:start_import_test) { described_class.import(object_structure) } + let(:object_structure) { [load_dynamic_field_json('text/default')] } + + it 'responds to skip_field?' do + expect(described_class).to respond_to('skip_field?') + end + + it 'imports OTRS DynamicFields' do + expect(Import::OTRS::DynamicField::Text).to receive(:new) + start_import_test + end +end diff --git a/spec/import/otrs/dynamic_field_spec.rb b/spec/import/otrs/dynamic_field_spec.rb new file mode 100644 index 000000000..0bf0829ed --- /dev/null +++ b/spec/import/otrs/dynamic_field_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField do + it_behaves_like 'Import::OTRS::DynamicField' + + let(:start_import_test) { described_class.new(object_structure) } + let(:object_structure) { load_dynamic_field_json('text/default') } + + it 'requires an implementation of init_callback' do + expect(ObjectManager::Attribute).to receive(:get).and_return(false) + expect { + start_import_test + }.to raise_error(RuntimeError) + end +end diff --git a/spec/import/otrs/history/article_spec.rb b/spec/import/otrs/history/article_spec.rb new file mode 100644 index 000000000..0b2b883e7 --- /dev/null +++ b/spec/import/otrs/history/article_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History::Article do + it_behaves_like 'Import::OTRS::History' + + it 'imports an OTRS AddNote history entry' do + + zammad_structure = { + id: '11307', + o_id: '3973', + history_type: 'created', + history_object: 'Ticket::Article', + related_o_id: '730', + related_history_object: 'Ticket', + created_at: '2014-11-21 00:21:08', + created_by_id: '3' + } + + history_from_json('article/default', zammad_structure) + end +end diff --git a/spec/import/otrs/history/move_spec.rb b/spec/import/otrs/history/move_spec.rb new file mode 100644 index 000000000..d00e931bc --- /dev/null +++ b/spec/import/otrs/history/move_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History::Move do + it_behaves_like 'Import::OTRS::History' + + it 'imports an OTRS Move history entry' do + + zammad_structure = { + id: '238', + o_id: '39', + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'group', + value_from: 'Source', + value_to: 'Target', + id_from: '5', + id_to: '2', + created_at: '2014-05-12 13:42:41', + created_by_id: '1' + } + + history_from_json('move/default', zammad_structure) + end +end diff --git a/spec/import/otrs/history/new_ticket_spec.rb b/spec/import/otrs/history/new_ticket_spec.rb new file mode 100644 index 000000000..ef1b33b4a --- /dev/null +++ b/spec/import/otrs/history/new_ticket_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History::NewTicket do + it_behaves_like 'Import::OTRS::History' + + it 'imports an OTRS NewTicket history entry' do + + zammad_structure = { + id: '11291', + o_id: '730', + history_type: 'created', + history_object: 'Ticket', + created_at: '2014-11-21 00:17:41', + created_by_id: '3' + } + + history_from_json('new_ticket/default', zammad_structure) + end +end diff --git a/spec/import/otrs/history/priority_update_spec.rb b/spec/import/otrs/history/priority_update_spec.rb new file mode 100644 index 000000000..f1e407695 --- /dev/null +++ b/spec/import/otrs/history/priority_update_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History::PriorityUpdate do + it_behaves_like 'Import::OTRS::History' + + it 'imports an OTRS PriorityUpdate history entry' do + + zammad_structure = { + id: '11131', + o_id: '721', + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'priority', + value_from: '2 low', + value_to: '4 high', + id_from: '2', + id_to: '4', + created_at: '2014-09-22 16:44:55', + created_by_id: '1' + } + + history_from_json('priority_update/default', zammad_structure) + end +end diff --git a/spec/import/otrs/history/state_update_spec.rb b/spec/import/otrs/history/state_update_spec.rb new file mode 100644 index 000000000..2a2494122 --- /dev/null +++ b/spec/import/otrs/history/state_update_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History::StateUpdate do + it_behaves_like 'Import::OTRS::History' + + it 'imports an OTRS StateUpdate history entry' do + + zammad_structure = { + id: '11305', + o_id: '730', + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'state', + value_from: 'new', + id_from: 1, + value_to: 'open', + id_to: 2, + created_at: '2014-11-21 00:21:08', + created_by_id: '3' + } + + history_from_json('state_update/default', zammad_structure) + end +end diff --git a/spec/import/otrs/history_examples.rb b/spec/import/otrs/history_examples.rb new file mode 100644 index 000000000..1b66f0b04 --- /dev/null +++ b/spec/import/otrs/history_examples.rb @@ -0,0 +1,19 @@ +require 'rails_helper' +require 'history' + +def history_from_json(file, zammad_structure) + expect(History).to receive(:add).with(zammad_structure) + described_class.new(load_history_json(file)) +end + +def load_history_json(file) + json_fixture("import/otrs/history/#{file}") +end + +RSpec.shared_examples 'Import::OTRS::History' do + it 'responds to init_callback' do + expect(History).to receive(:add) + blank_instance = described_class.new({}) + expect(blank_instance).to respond_to('init_callback') + end +end diff --git a/spec/import/otrs/history_factory_spec.rb b/spec/import/otrs/history_factory_spec.rb new file mode 100644 index 000000000..838394754 --- /dev/null +++ b/spec/import/otrs/history_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::HistoryFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/history_spec.rb b/spec/import/otrs/history_spec.rb new file mode 100644 index 000000000..6ca040ed6 --- /dev/null +++ b/spec/import/otrs/history_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History do + + let(:start_import_test) { described_class.new(object_structure) } + let(:object_structure) { load_history_json('article/default') } + + it 'requires an implementation of init_callback' do + expect { + start_import_test + }.to raise_error(RuntimeError) + end +end diff --git a/spec/import/otrs/import_stats_examples.rb b/spec/import/otrs/import_stats_examples.rb new file mode 100644 index 000000000..e21cfb5c7 --- /dev/null +++ b/spec/import/otrs/import_stats_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'Import::OTRS::ImportStats' do + it 'responds to current_state' do + expect(described_class).to respond_to('current_state') + end +end diff --git a/spec/import/otrs/priority_factory_spec.rb b/spec/import/otrs/priority_factory_spec.rb new file mode 100644 index 000000000..34033008c --- /dev/null +++ b/spec/import/otrs/priority_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::PriorityFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/priority_spec.rb b/spec/import/otrs/priority_spec.rb new file mode 100644 index 000000000..c7c5a3f6e --- /dev/null +++ b/spec/import/otrs/priority_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Priority do + + def creates_with(zammad_structure) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_priority_json(file) + json_fixture("import/otrs/priority/#{file}") + end + + let(:import_object) { Ticket::Priority } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'default' do + + let(:object_structure) { load_priority_json('default') } + let(:zammad_structure) { + { + created_by_id: '1', + updated_by_id: '1', + active: true, + updated_at: '2014-04-28 10:53:18', + created_at: '2014-04-28 10:53:18', + name: '4 high', + id: '4' + } + } + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/queue_factory_spec.rb b/spec/import/otrs/queue_factory_spec.rb new file mode 100644 index 000000000..9e89bd0f6 --- /dev/null +++ b/spec/import/otrs/queue_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::QueueFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/queue_spec.rb b/spec/import/otrs/queue_spec.rb new file mode 100644 index 000000000..4b0621ee9 --- /dev/null +++ b/spec/import/otrs/queue_spec.rb @@ -0,0 +1,51 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Queue do + + def creates_with(zammad_structure) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_queue_json(file) + json_fixture("import/otrs/queue/#{file}") + end + + let(:import_object) { Group } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'default' do + + let(:object_structure) { load_queue_json('default') } + let(:zammad_structure) { + { + created_by_id: 1, + updated_by_id: 1, + active: false, + updated_at: '2014-05-13 10:54:11', + created_at: '2014-05-13 10:54:11', + name: 'UnitTestQueue45699', + id: '11', + note: 'Some comment' + } + } + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/requester_spec.rb b/spec/import/otrs/requester_spec.rb new file mode 100644 index 000000000..f7b77cc4a --- /dev/null +++ b/spec/import/otrs/requester_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Requester do + it 'responds to load' do + expect(described_class).to respond_to('load') + end + + it 'responds to list' do + expect(described_class).to respond_to('list') + end + + it 'responds to connection_test' do + expect(described_class).to respond_to('connection_test') + end + + context 'caching request results' do + + let(:response) { + response = double() + response_body = double() + expect(response_body).to receive(:to_s).at_least(:once).and_return('{"Result": {}}') + expect(response).to receive('success?').at_least(:once).and_return(true) + expect(response).to receive('body').at_least(:once).and_return(response_body) + response + } + + it 'is active if no args are given' do + expect(UserAgent).to receive(:post).and_return(response) + described_class.load('Ticket') + described_class.load('Ticket') + end + + it 'is not active if args are given' do + expect(UserAgent).to receive(:post).twice.and_return(response) + described_class.load('Ticket', offset: 10) + described_class.load('Ticket', offset: 20) + end + end +end diff --git a/spec/import/otrs/state_factory_spec.rb b/spec/import/otrs/state_factory_spec.rb new file mode 100644 index 000000000..40764dca7 --- /dev/null +++ b/spec/import/otrs/state_factory_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' +require 'import/transaction_factory_examples' + +RSpec.describe Import::OTRS::StateFactory do + it_behaves_like 'Import::TransactionFactory' + + it 'creates a state backup in the pre_import_hook' do + expect(described_class).to receive(:backup) + described_class.pre_import_hook([]) + end +end diff --git a/spec/import/otrs/state_spec.rb b/spec/import/otrs/state_spec.rb new file mode 100644 index 000000000..5e13ba04f --- /dev/null +++ b/spec/import/otrs/state_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::State do + + def creates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(nil) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_state_json(file) + json_fixture("import/otrs/state/#{file}") + end + + let(:import_object) { Ticket::State } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'closed' do + + let(:object_structure) { load_state_json('default') } + let(:zammad_structure) { + { + created_by_id: 1, + updated_by_id: 1, + active: '1', + state_type_id: 5, + updated_at: '2014-04-28 10:53:18', + created_at: '2014-04-28 10:53:18', + name: 'closed successful', + id: '2', + note: 'Ticket is closed successful.' + } + } + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/sys_config_factory_spec.rb b/spec/import/otrs/sys_config_factory_spec.rb new file mode 100644 index 000000000..119568af4 --- /dev/null +++ b/spec/import/otrs/sys_config_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/import_factory_examples' + +RSpec.describe Import::OTRS::SysConfigFactory do + it_behaves_like 'Import factory' +end diff --git a/spec/import/otrs/ticket_factory_spec.rb b/spec/import/otrs/ticket_factory_spec.rb new file mode 100644 index 000000000..fdcb9b201 --- /dev/null +++ b/spec/import/otrs/ticket_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::TicketFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/ticket_spec.rb b/spec/import/otrs/ticket_spec.rb new file mode 100644 index 000000000..66a940e8a --- /dev/null +++ b/spec/import/otrs/ticket_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Ticket do + + def creates_with(zammad_structure) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_ticket_json(file) + json_fixture("import/otrs/ticket/#{file}") + end + + def import_backend_expectations + expect(Import::OTRS::ArticleCustomerFactory).to receive(:import) + expect(Import::OTRS::ArticleFactory).to receive(:import) + expect(Import::OTRS::HistoryFactory).to receive(:import) + expect(User).to receive(:find_by).twice.and_return(nil) + # needed, otherwise 'ActiveRecord::UnknownAttributeError' for + # DynamicFields will arise + allow(Import::OTRS::DynamicFieldFactory).to receive('skip_field?').and_return(true) + end + + let(:import_object) { Ticket } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'default' do + + let(:object_structure) { load_ticket_json('default') } + let(:zammad_structure) { + { + title: 'test #3', + owner_id: 1, + customer_id: 1, + created_by_id: '3', + updated_by_id: 1, + updated_at: '2014-11-21 00:21:08', + created_at: '2014-11-21 00:17:40', + number: '20141121305000012', + group_id: '1', + state_id: '2', + priority_id: '3', + id: '730', + close_at: '2014-11-21 00:21:08' + } + } + + it 'creates' do + import_backend_expectations + creates_with(zammad_structure) + end + + it 'updates' do + import_backend_expectations + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/user_factory_spec.rb b/spec/import/otrs/user_factory_spec.rb new file mode 100644 index 000000000..b88035417 --- /dev/null +++ b/spec/import/otrs/user_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::UserFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/user_spec.rb b/spec/import/otrs/user_spec.rb new file mode 100644 index 000000000..478821e26 --- /dev/null +++ b/spec/import/otrs/user_spec.rb @@ -0,0 +1,131 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::User do + + def creates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(nil) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:role_ids).and_return([]) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_user_json(file) + json_fixture("import/otrs/user/#{file}") + end + + def prepare_expectations + requester_expectations + user_expectations + end + + def user_expectations + expect(User).to receive(:where).and_return([]) + end + + # this is really bad and should get improved! + # these are integration tests that will likely fail + # soon - sorry :) + def requester_expectations + queue_list = [ + { + 'QueueID' => '1', + 'GroupID' => '2', + } + ] + expect(Import::OTRS::Requester).to receive(:load).with('Queue').and_return(queue_list) + + group_list = [ + { + 'ID' => '2', + 'Name' => 'user', + }, + { + 'ID' => '3', + 'Name' => 'another_group', + }, + ] + expect(Import::OTRS::Requester).to receive(:load).with('Group').and_return(group_list) + + role_list = [{ 'ID' => '3', 'GroupIDs' => %w(2 3) }] + expect(Import::OTRS::Requester).to receive(:load).with('Role').and_return(role_list) + end + + let(:import_object) { ::User } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'default' do + + let(:object_structure) { load_user_json('default') } + let(:zammad_structure) { + { + created_by_id: 1, + updated_by_id: 1, + active: true, + source: 'OTRS Import', + role_ids: [2], + group_ids: ['1'], + password: '9faaba2ab242a99bbb6992e9424386375f6757c17e6484ae570f39d9cad9f28ea', + updated_at: '2014-04-28 10:53:18', + created_at: '2014-04-28 10:53:18', + id: '1', + email: 'root@localhost', + firstname: 'Admin', + lastname: 'OTRS', + login: 'root@localhost' + } + } + + it 'creates' do + prepare_expectations + creates_with(zammad_structure) + end + + it 'updates' do + prepare_expectations + updates_with(zammad_structure) + end + end + + context 'no groups' do + + let(:object_structure) { load_user_json('no_groups') } + let(:zammad_structure) { + { + created_by_id: 1, + updated_by_id: 1, + active: true, + source: 'OTRS Import', + role_ids: [2], + group_ids: [], + password: '9edb001ad7900daea0622d89225c9ca729749fd12ae5ea044f072d1b7c56c8cc', + updated_at: '2014-11-14 00:53:20', + created_at: '2014-11-14 00:53:20', + id: '6', + email: 'agent-2-for-role-2@example.com', + firstname: 'agent-2-for-role-2', + lastname: 'agent-2-for-role-2', + login: 'agent-2-for-role-2' + } + } + + it 'creates' do + prepare_expectations + creates_with(zammad_structure) + end + + it 'updates' do + prepare_expectations + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs_spec.rb b/spec/import/otrs_spec.rb new file mode 100644 index 000000000..95bb5a10f --- /dev/null +++ b/spec/import/otrs_spec.rb @@ -0,0 +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' + +RSpec.describe Import::OTRS do + it_behaves_like 'Import backend' + it_behaves_like 'Import::Helper' + it_behaves_like 'Import::OTRS::Async' + it_behaves_like 'Import::OTRS::Diff' + it_behaves_like 'Import::OTRS::ImportStats' +end diff --git a/spec/import/transaction_factory_examples.rb b/spec/import/transaction_factory_examples.rb new file mode 100644 index 000000000..3abe21c6d --- /dev/null +++ b/spec/import/transaction_factory_examples.rb @@ -0,0 +1,5 @@ +require 'import/base_factory_examples' + +RSpec.shared_examples 'Import::TransactionFactory' do + it_behaves_like 'Import::BaseFactory' +end diff --git a/spec/import/transaction_factory_spec.rb b/spec/import/transaction_factory_spec.rb new file mode 100644 index 000000000..d030be279 --- /dev/null +++ b/spec/import/transaction_factory_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' +require 'import/transaction_factory_examples' + +RSpec.describe Import::TransactionFactory do + it_behaves_like 'Import::TransactionFactory' + it_behaves_like 'Import::BaseFactory extender' +end diff --git a/spec/import/zendesk_spec.rb b/spec/import/zendesk_spec.rb new file mode 100644 index 000000000..7404bbe04 --- /dev/null +++ b/spec/import/zendesk_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/importer_examples' + +RSpec.describe Import::Zendesk do + it_behaves_like 'Import backend' +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 000000000..0c2dfd638 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,57 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../../config/environment', __FILE__) +# Prevent database truncation if the environment is production +abort('The Rails environment is running in production mode!') if Rails.env.production? +require 'spec_helper' +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } + +# Checks for pending migration and applies them before tests are run. +# If you are not using ActiveRecord, you can remove this line. +ActiveRecord::Migration.maintain_test_schema! + +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, :type => :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 000000000..1563cc98a --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,107 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# The `.rspec` file also contains a few flags that are not defaults but that +# users commonly want. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +require 'spec_helper/zammad_helper' + +RSpec.configure do |config| + # Zammad specific helpers + config.include ZammadHelper + + # skip Zammad helper functions in the stacktrace to lower noise + config.backtrace_exclusion_patterns << %r{/spec/spec_helper/} + + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/spec/spec_helper/zammad_helper.rb b/spec/spec_helper/zammad_helper.rb new file mode 100644 index 000000000..30dbf3859 --- /dev/null +++ b/spec/spec_helper/zammad_helper.rb @@ -0,0 +1 @@ +require 'spec_helper/zammad_helper/json' diff --git a/spec/spec_helper/zammad_helper/json.rb b/spec/spec_helper/zammad_helper/json.rb new file mode 100644 index 000000000..fc65a810e --- /dev/null +++ b/spec/spec_helper/zammad_helper/json.rb @@ -0,0 +1,5 @@ +module ZammadHelper + def json_fixture(file) + JSON.parse(File.read("spec/fixtures/#{file}.json")) + end +end