Added zendesk import browser tests.

This commit is contained in:
Thorsten Eckel 2016-01-18 20:32:34 +01:00
parent d45f634e05
commit 64d722d14c
7 changed files with 565 additions and 50 deletions

View file

@ -404,3 +404,13 @@ job_integration_autowizard_chrome:
- ruby -I test/ test/integration/auto_wizard_test.rb || script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1
- script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT
job_integration_zendesk_chrome:
- export BROWSER_PORT=4071
- export WS_PORT=4072
- export BROWSER_URL=http://$IP:$BROWSER_PORT
- RAILS_ENV=test rake db:create
- script/bootstrap.sh
- rake assets:precompile
- script/build/test_startup.sh $RAILS_ENV $BROWSER_PORT $WS_PORT
- ruby -I test/ test/integration/zendesk_import_browser_test.rb || script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1
- script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT

View file

@ -1,14 +1,21 @@
class Index extends App.ControllerContent
className: 'getstarted fit'
elements:
'.input-feedback': 'urlStatus'
'[data-target=otrs-start-migration]': 'nextStartMigration'
'.otrs-link-error': 'linkErrorMessage'
'.input-feedback': 'urlStatus'
'[data-target=zendesk-credentials]': 'nextEnterCredentials'
'[data-target=zendesk-start-migration]': 'nextStartMigration'
'#zendesk-url': 'zendeskUrl'
'.js-zendeskUrlApiToken': 'zendeskUrlApiToken'
'.zendesk-url-error': 'linkErrorMessage'
'.zendesk-api-token-error': 'apiTokenErrorMessage'
'#zendesk-email': 'zendeskEmail'
'#zendesk-api-token': 'zendeskApiToken'
events:
'click .js-otrs-link': 'showLink'
'click .js-download': 'startDownload'
'click .js-migration-start': 'startMigration'
'keyup #otrs-link': 'updateUrl'
'click .js-zendesk-credentials': 'showCredentials'
'click .js-migration-start': 'startMigration'
'keyup #zendesk-url': 'updateUrl'
'keyup #zendesk-api-token': 'updateApiToken'
constructor: ->
super
@ -34,7 +41,7 @@ class Index extends App.ControllerContent
return
# check if import is active
if data.import_mode == true && data.import_backend != 'otrs'
if data.import_mode == true && data.import_backend != 'zendesk'
@navigate '#import/' + data.import_backend
return
@ -47,34 +54,19 @@ class Index extends App.ControllerContent
)
render: ->
@html App.view('import/otrs')()
startDownload: (e) =>
e.preventDefault()
@$('.js-otrs-link').removeClass('hide')
showLink: (e) =>
e.preventDefault()
@$('[data-slide=otrs-plugin]').toggleClass('hide')
@$('[data-slide=otrs-link]').toggleClass('hide')
showImportState: =>
@$('[data-slide=otrs-plugin]').addClass('hide')
@$('[data-slide=otrs-link]').addClass('hide')
@$('[data-slide=otrs-import]').removeClass('hide')
@html App.view('import/zendesk')()
updateUrl: (e) =>
url = $(e.target).val()
@urlStatus.attr('data-state', 'loading')
@linkErrorMessage.text('')
# get data
callback = =>
@ajax(
id: 'import_otrs_url',
id: 'import_zendesk_url',
type: 'POST',
url: @apiPath + '/import/otrs/url_check',
data: JSON.stringify(url: url)
url: @apiPath + '/import/zendesk/url_check',
data: JSON.stringify(url: @zendeskUrl.val())
processData: true,
success: (data, status, xhr) =>
@ -83,14 +75,55 @@ class Index extends App.ControllerContent
if data.result is 'ok'
@urlStatus.attr('data-state', 'success')
@linkErrorMessage.text('')
@nextStartMigration.removeClass('hide')
@nextEnterCredentials.removeClass('hide')
else
@urlStatus.attr('data-state', 'error')
@linkErrorMessage.text( data.message_human || data.message )
@nextEnterCredentials.addClass('hide')
)
@delay( callback, 700, 'import_zendesk_url' )
updateApiToken: (e) =>
@urlStatus.attr('data-state', 'loading')
@apiTokenErrorMessage.text('')
# get data
callback = =>
@ajax(
id: 'import_zendesk_api_token',
type: 'POST',
url: @apiPath + '/import/zendesk/credentials_check',
data: JSON.stringify(username: @zendeskEmail.val(), token: @zendeskApiToken.val())
processData: true,
success: (data, status, xhr) =>
# validate form
console.log(data)
if data.result is 'ok'
@urlStatus.attr('data-state', 'success')
@apiTokenErrorMessage.text('')
@nextStartMigration.removeClass('hide')
else
@urlStatus.attr('data-state', 'error')
@apiTokenErrorMessage.text( data.message_human || data.message )
@nextStartMigration.addClass('hide')
)
@delay( callback, 700, 'import_otrs_url' )
@delay( callback, 700, 'import_zendesk_api_token' )
showCredentials: (e) =>
e.preventDefault()
@urlStatus.attr('data-state', '')
@zendeskUrlApiToken.attr('href', @zendeskUrl.val() + "agent/admin/api")
@zendeskUrlApiToken.val('HERE')
@$('[data-slide=zendesk-url]').toggleClass('hide')
@$('[data-slide=zendesk-credentials]').toggleClass('hide')
showImportState: =>
@$('[data-slide=zendesk-url]').addClass('hide')
@$('[data-slide=zendesk-credentials]').addClass('hide')
@$('[data-slide=zendesk-import]').removeClass('hide')
startMigration: (e) =>
e.preventDefault()
@ -98,7 +131,7 @@ class Index extends App.ControllerContent
@ajax(
id: 'import_start',
type: 'POST',
url: @apiPath + '/import/otrs/import_start',
url: @apiPath + '/import/zendesk/import_start',
processData: true,
success: (data, status, xhr) =>
@ -108,19 +141,17 @@ class Index extends App.ControllerContent
@delay( @updateMigration, 3000 )
)
updateMigration: =>
@showImportState()
@ajax(
id: 'import_status',
type: 'GET',
url: @apiPath + '/import/otrs/import_status',
url: @apiPath + '/import/zendesk/import_status',
processData: true,
success: (data, status, xhr) =>
if data.setup_done
@Config.set('system_init_done', true)
@navigate '#'
if data.result is 'import_done'
window.location.reload()
return
for key, item of data.data

View file

@ -0,0 +1,108 @@
<div class="main flex vertical centered darkBackground">
<%- @Icon('full-logo', 'wizard-logo') %>
<div class="import wizard">
<div class="wizard-slide vertical" data-slide="zendesk-url">
<h2><%- @T('Zendesk URL') %></h2>
<div class="wizard-body flex vertical justified">
<p>
<%- @T('Enter the URL of your Zendesk system') %>:
</p>
<div class="form-group">
<label for="zendesk-url">Zendesk URL</label>
<div class="u-positionOrigin">
<input type="url" id="zendesk-url" class="form-control" placeholder="https://example.zendesk.com/" name="zendesk-url">
<div class="input-feedback centered">
<div class="small loading icon"></div>
<%- @Icon('diagonal-cross', 'icon-error') %>
<%- @Icon('checkmark') %>
</div>
</div>
<div class="error zendesk-url-error"></div>
</div>
</div>
<div class="wizard-controls horizontal center">
<a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>
<div class="btn btn--primary align-right hide js-zendesk-credentials" data-target="zendesk-credentials"><%- @T('Enter credentials') %></div>
</div>
</div>
<div class="wizard-slide vertical hide" data-slide="zendesk-credentials">
<h2><%- @T('Zendesk credentials') %></h2>
<div class="wizard-body flex vertical justified">
<p>
<%- @T('Enter your Email address and the Zendesk API token gained from your') %> <a class="js-zendeskUrlApiToken" target="_blank"><%- @T('admin interface') %></a>
</p>
<div class="form-group">
<label for="zendesk-email">Email address</label>
<div class="u-positionOrigin">
<input type="email" id="zendesk-email" class="form-control" placeholder="admin@example.com" name="zendesk-email">
</div>
<label for="zendesk-api-token">API token</label>
<div class="u-positionOrigin">
<input type="email" id="zendesk-api-token" class="form-control" placeholder="XYZ3133723421111" name="zendesk-api-token">
<div class="input-feedback centered">
<div class="small loading icon"></div>
<%- @Icon('diagonal-cross', 'icon-error') %>
<%- @Icon('checkmark') %>
</div>
</div>
<div class="error zendesk-api-token-error"></div>
</div>
</div>
<div class="wizard-controls horizontal center">
<a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>
<div class="btn btn--primary align-right hide js-migration-start" data-target="zendesk-start-migration"><%- @T('Migrate Zendesk Data') %></div>
</div>
</div>
<div class="wizard-slide vertical hide" data-slide="zendesk-import">
<h2><%- @T('Zendesk Migration') %></h2>
<div class="alert alert--danger hide js-error" role="alert"></div>
<div class="wizard-body flex vertical justified">
<table class="progressTable">
<tr class="js-group">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Groups') %></span>
<td class="progressTable-progressCell">
<div class="horizontal center">
<div class="flex"><progress max="42" value="42"></progress></div>
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-organization">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Organizations') %></span>
<td class="progressTable-progressCell">
<div class="horizontal center">
<div class="flex"><progress max="42" value="42"></progress></div>
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-user">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Users') %></span>
<td class="progressTable-progressCell">
<div class="horizontal center">
<div class="flex"><progress max="42" value="42"></progress></div>
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-ticket">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Tickets') %></span>
<td class="progressTable-progressCell">
<div class="horizontal center">
<div class="flex"><progress max="42" value="42"></progress></div>
<%- @Icon('checkmark') %>
</div>
</tr>
</table>
</div>
<div class="wizard-controls horizontal center">
<a href="#" class="btn btn--primary align-right hide js-finished"><%- @T('Done') %></a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,124 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
require 'zendesk_api'
class ImportZendeskController < ApplicationController
def url_check
return if setup_done_response
# validate
if !params[:url] || params[:url] !~ %r{^(http|https)://.+?$}
render json: {
result: 'invalid',
message: 'Invalid URL!',
}
return
end
# connection test
translation_map = {
'No such file' => 'Hostname not found!',
'getaddrinfo: nodename nor servname provided, or not known' => 'Hostname not found!',
'No route to host' => 'No route to host!',
'Connection refused' => 'Connection refused!',
}
response = UserAgent.request( params[:url] )
if !response.success?
message_human = ''
translation_map.each {|key, message|
if response.error.to_s =~ /#{Regexp.escape(key)}/i
message_human = message
end
}
render json: {
result: 'invalid',
message_human: message_human,
message: response.error.to_s,
}
return
end
Setting.set('import_zendesk_endpoint', "#{params[:url]}api/v2")
render json: {
result: 'ok',
url: params[:url],
}
end
def credentials_check
return if setup_done_response
if !params[:username] || !params[:token]
render json: {
result: 'invalid',
message_human: 'Incomplete credentials',
}
return
end
Setting.set('import_zendesk_endpoint_username', params[:username])
Setting.set('import_zendesk_endpoint_key', params[:token])
if !Import::Zendesk.connection_test
Setting.set('import_zendesk_endpoint_username', nil)
Setting.set('import_zendesk_endpoint_key', nil)
render json: {
result: 'invalid',
message_human: 'Invalid credentials!',
}
return
end
render json: {
result: 'ok',
}
end
def import_start
return if setup_done_response
Setting.set('import_mode', true)
# start migration
Import::Zendesk.delay.start_bg
render json: {
result: 'ok',
}
end
def import_status
result = Import::Zendesk.status_bg
if result[:result] == 'import_done'
Setting.reload
end
render json: result
end
private
def setup_done
count = User.all.count()
done = true
if count <= 2
done = false
end
done
end
def setup_done_response
if !setup_done
return false
end
render json: {
setup_done: true,
}
true
end
end

View file

@ -0,0 +1,10 @@
Zammad::Application.routes.draw do
api_path = Rails.configuration.api_path
# import zendesk
match api_path + '/import/zendesk/url_check', to: 'import_zendesk#url_check', via: :post
match api_path + '/import/zendesk/credentials_check', to: 'import_zendesk#credentials_check', via: :post
match api_path + '/import/zendesk/import_start', to: 'import_zendesk#import_start', via: :post
match api_path + '/import/zendesk/import_status', to: 'import_zendesk#import_status', via: :get
end

View file

@ -48,6 +48,95 @@ module Import::Zendesk
true
end
=begin
start import in background
Import::Zendesk.start_bg
=end
def start_bg
Setting.reload
Import::Zendesk.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 thread to import data
begin
import_thread = Thread.new {
Import::Zendesk.start
}
rescue => e
status_update_thread.exit
status_update_thread.join
Rails.logger.error e.message
Rails.logger.error e.backtrace.inspect
result = {
message: e.message,
result: 'error',
}
Cache.write('import:state', result, expires_in: 10.hours)
return false
end
import_thread.join
status_update_thread.exit
status_update_thread.join
result = {
result: 'import_done',
}
Cache.write('import:state', result, expires_in: 10.hours)
Setting.set('system_init_done', true)
Setting.set('import_mode', false)
end
=begin
get import state from background process
result = Import::Zendesk.status_bg
=end
def status_bg
state = Cache.get('import:state')
return state if state
{
message: 'not running',
}
end
=begin
start get request to backend to check connection
result = connection_test
return
true | false
=end
def connection_test
initialize_client
return true if @client.users.first
false
end
def statistic
# check cache
@ -89,9 +178,61 @@ module Import::Zendesk
statistic
end
def initialize_client
return nil if @client
=begin
return current import state
result = current_state
return
{
:Group => {
:total => 1234,
:done => 13,
},
:Organization => {
:total => 1234,
:done => 13,
},
:User => {
:total => 1234,
:done => 13,
},
:Ticket => {
:total => 1234,
:done => 13,
},
}
=end
def current_state
data = statistic
# TODO: Ticket, User, Organization fields
{
Group: {
done: Group.count,
total: data['Groups'] || 0,
},
Organization: {
done: Organization.count,
total: data['Organizations'] || 0,
},
User: {
done: User.count,
total: data['Users'] || 0,
},
Ticket: {
done: Ticket.count,
total: data['Tickets'] || 0,
},
}
end
def initialize_client
@client = ZendeskAPI::Client.new do |config|
config.url = Setting.get('import_zendesk_endpoint')
@ -350,11 +491,13 @@ module Import::Zendesk
@client.organizations.each { |zendesk_organization|
local_organization_fields = {
name: zendesk_organization.name,
note: zendesk_organization.note,
shared: zendesk_organization.shared_tickets,
name: zendesk_organization.name,
note: zendesk_organization.note,
shared: zendesk_organization.shared_tickets,
# shared: zendesk_organization.shared_comments, # TODO, not yet implemented
# }.merge(zendesk_organization.organization_fields) # TODO
updated_by_id: 1,
created_by_id: 1
}
local_organization = Organization.create_if_not_exists( local_organization_fields )
@ -391,6 +534,8 @@ module Import::Zendesk
verified: zendesk_user.verified,
organization_id: @zendesk_organization_mapping[ zendesk_user.organization_id ],
last_login: zendesk_user.last_login_at,
updated_by_id: 1,
created_by_id: 1
}
if @zendesk_user_group_mapping[ zendesk_user.id ]
@ -498,19 +643,18 @@ module Import::Zendesk
title: zendesk_ticket.subject,
note: zendesk_ticket.description,
group_id: @zendesk_group_mapping[ zendesk_ticket.group_id ] || 1,
customer_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ],
customer_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ] || 1,
organization_id: @zendesk_organization_mapping[ zendesk_ticket.organization_id ],
state: Ticket::State.lookup( name: mapping_state( zendesk_ticket.status ) ),
priority: Ticket::Priority.lookup( name: mapping_priority( zendesk_ticket.priority ) ),
pending_time: zendesk_ticket.due_at,
updated_at: zendesk_ticket.updated_at,
created_at: zendesk_ticket.created_at,
updated_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ],
created_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ],
updated_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ] || 1,
created_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ] || 1,
# }.merge(zendesk_ticket_fields) TODO
}
ticket_author = User.find( @zendesk_user_mapping[ zendesk_ticket.requester_id ] )
ticket_author = User.find( @zendesk_user_mapping[ zendesk_ticket.requester_id ] || 1 )
local_ticket_fields[:create_article_sender_id] = if ticket_author.role?('Customer')
article_sender_customer.id
@ -553,7 +697,7 @@ module Import::Zendesk
object: 'Ticket',
o_id: local_ticket.id,
item: tag.id,
created_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ],
created_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ] || 1,
)
}
@ -581,11 +725,11 @@ module Import::Zendesk
ticket_id: local_ticket.id,
body: zendesk_article.html_body,
internal: !zendesk_article.public,
updated_by_id: @zendesk_user_mapping[ zendesk_article.author_id ],
created_by_id: @zendesk_user_mapping[ zendesk_article.author_id ],
updated_by_id: @zendesk_user_mapping[ zendesk_article.author_id ] || 1,
created_by_id: @zendesk_user_mapping[ zendesk_article.author_id ] || 1,
}
article_author = User.find( @zendesk_user_mapping[ zendesk_article.author_id ] )
article_author = User.find( @zendesk_user_mapping[ zendesk_article.author_id ] || 1 )
local_article_fields[:sender_id] = if article_author.role?('Customer')
article_sender_customer.id
@ -662,7 +806,8 @@ module Import::Zendesk
filename: zendesk_attachment.file_name,
preferences: {
'Content-Type' => zendesk_attachment.content_type
}
},
created_by_id: 1
)
}
}

View file

@ -0,0 +1,87 @@
# encoding: utf-8
require 'browser_test_helper'
class ZendeskImportBrowserTest < TestCase
def test_import
if !ENV['IMPORT_BT_ZENDESK_ENDPOINT']
fail "ERROR: Need IMPORT_BT_ZENDESK_ENDPOINT - hint IMPORT_BT_ZENDESK_ENDPOINT='https://example.zendesk.com/' (including trailing slash!)"
end
if !ENV['IMPORT_BT_ZENDESK_ENDPOINT_USERNAME']
fail "ERROR: Need IMPORT_BT_ZENDESK_ENDPOINT_USERNAME - hint IMPORT_BT_ZENDESK_ENDPOINT_USERNAME='your@email.com'"
end
if !ENV['IMPORT_BT_ZENDESK_ENDPOINT_KEY']
fail "ERROR: Need IMPORT_BT_ZENDESK_ENDPOINT_KEY - hint IMPORT_BT_ZENDESK_ENDPOINT_KEY='XYZ3133723421111'"
end
@browser = browser_instance
location(url: browser_url)
click(css: 'a[href="#import"]')
click(css: 'a[href="#import/zendesk"]')
set(
css: '#zendesk-url',
value: 'https://reallybadexample.zendesk.com/'
)
sleep 5
watch_for(
css: '.zendesk-url-error',
value: 'Hostname not found!',
)
set(
css: '#zendesk-url',
value: ENV['IMPORT_BT_ZENDESK_ENDPOINT']
)
sleep 5
watch_for_disappear(
css: '.zendesk-url-error',
value: 'Hostname not found!',
)
click(css: '.js-zendesk-credentials')
set(
css: '#zendesk-email',
value: ENV['IMPORT_BT_ZENDESK_ENDPOINT_USERNAME']
)
set(
css: '#zendesk-api-token',
value: '1nv4l1dT0K3N'
)
sleep 5
watch_for(
css: '.zendesk-api-token-error',
value: 'Invalid credentials!',
)
set(
css: '#zendesk-api-token',
value: ENV['IMPORT_BT_ZENDESK_ENDPOINT_KEY']
)
sleep 5
watch_for_disappear(
css: '.zendesk-url-error',
value: 'Invalid credentials!',
)
click(css: '.js-migration-start')
watch_for(
css: 'body',
value: 'login',
timeout: 300,
)
end
end