embulk-input-zendesk 0.2.14 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +9 -3
- data/.travis.yml +5 -44
- data/CHANGELOG.md +3 -0
- data/README.md +5 -5
- data/build.gradle +123 -0
- data/classpath/commons-codec-1.10.jar +0 -0
- data/classpath/commons-logging-1.2.jar +0 -0
- data/classpath/embulk-input-zendesk-0.3.0.jar +0 -0
- data/classpath/httpclient-4.5.6.jar +0 -0
- data/classpath/httpcore-4.4.10.jar +0 -0
- data/config/checkstyle/checkstyle.xml +128 -0
- data/config/checkstyle/default.xml +108 -0
- data/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/gradle/wrapper/gradle-wrapper.properties +5 -0
- data/gradlew +172 -0
- data/gradlew.bat +84 -0
- data/lib/embulk/guess/zendesk.rb +21 -0
- data/lib/embulk/input/zendesk.rb +3 -9
- data/src/main/java/org/embulk/input/zendesk/ZendeskInputPlugin.java +471 -0
- data/src/main/java/org/embulk/input/zendesk/clients/ZendeskRestClient.java +268 -0
- data/src/main/java/org/embulk/input/zendesk/models/AuthenticationMethod.java +23 -0
- data/src/main/java/org/embulk/input/zendesk/models/Target.java +46 -0
- data/src/main/java/org/embulk/input/zendesk/models/ZendeskException.java +25 -0
- data/src/main/java/org/embulk/input/zendesk/services/ZendeskSupportAPIService.java +109 -0
- data/src/main/java/org/embulk/input/zendesk/utils/ZendeskConstants.java +61 -0
- data/src/main/java/org/embulk/input/zendesk/utils/ZendeskDateUtils.java +51 -0
- data/src/main/java/org/embulk/input/zendesk/utils/ZendeskUtils.java +150 -0
- data/src/main/java/org/embulk/input/zendesk/utils/ZendeskValidatorUtils.java +92 -0
- data/src/test/java/org/embulk/input/zendesk/TestZendeskInputPlugin.java +232 -0
- data/src/test/java/org/embulk/input/zendesk/clients/TestZendeskRestClient.java +351 -0
- data/src/test/java/org/embulk/input/zendesk/services/TestZendeskSupportAPIService.java +172 -0
- data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskDateUtils.java +36 -0
- data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskUtil.java +160 -0
- data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskValidatorUtils.java +138 -0
- data/src/test/java/org/embulk/input/zendesk/utils/ZendeskPluginTestRuntime.java +133 -0
- data/src/test/java/org/embulk/input/zendesk/utils/ZendeskTestHelper.java +63 -0
- data/src/test/resources/config/base.yml +14 -0
- data/src/test/resources/config/base_validator.yml +48 -0
- data/src/test/resources/config/incremental.yml +54 -0
- data/src/test/resources/config/non-incremental.yml +39 -0
- data/src/test/resources/config/util.yml +18 -0
- data/src/test/resources/data/client.json +293 -0
- data/src/test/resources/data/error_data.json +187 -0
- data/src/test/resources/data/expected/ticket_column.json +148 -0
- data/src/test/resources/data/expected/ticket_column_with_related_objects.json +152 -0
- data/src/test/resources/data/expected/ticket_fields_column.json +92 -0
- data/src/test/resources/data/expected/ticket_metrics_column.json +98 -0
- data/src/test/resources/data/ticket_fields.json +225 -0
- data/src/test/resources/data/ticket_metrics.json +397 -0
- data/src/test/resources/data/ticket_with_related_objects.json +67 -0
- data/src/test/resources/data/tickets.json +232 -0
- data/src/test/resources/data/tickets_continue.json +52 -0
- data/src/test/resources/data/util.json +19 -0
- data/src/test/resources/data/util_page.json +227 -0
- metadata +65 -221
- data/.ruby-version +0 -1
- data/.travis.yml.erb +0 -43
- data/Gemfile +0 -2
- data/Rakefile +0 -21
- data/embulk-input-zendesk.gemspec +0 -29
- data/gemfiles/embulk-0.8.0-latest +0 -4
- data/gemfiles/embulk-0.8.1 +0 -4
- data/gemfiles/embulk-latest +0 -4
- data/gemfiles/template.erb +0 -4
- data/lib/embulk/input/zendesk/client.rb +0 -434
- data/lib/embulk/input/zendesk/plugin.rb +0 -199
- data/test/capture_io.rb +0 -45
- data/test/embulk/input/zendesk/test_client.rb +0 -722
- data/test/embulk/input/zendesk/test_plugin.rb +0 -628
- data/test/fixture_helper.rb +0 -11
- data/test/fixtures/invalid_app_marketplace_lack_one_property.yml +0 -13
- data/test/fixtures/invalid_app_marketplace_lack_two_property.yml +0 -12
- data/test/fixtures/invalid_lack_username.yml +0 -9
- data/test/fixtures/invalid_unknown_auth.yml +0 -9
- data/test/fixtures/tickets.json +0 -44
- data/test/fixtures/valid_app_marketplace.yml +0 -14
- data/test/fixtures/valid_auth_basic.yml +0 -11
- data/test/fixtures/valid_auth_oauth.yml +0 -10
- data/test/fixtures/valid_auth_token.yml +0 -11
- data/test/override_assert_raise.rb +0 -21
- data/test/run-test.rb +0 -26
@@ -1,199 +0,0 @@
|
|
1
|
-
require 'perfect_retry'
|
2
|
-
|
3
|
-
module Embulk
|
4
|
-
module Input
|
5
|
-
module Zendesk
|
6
|
-
class Plugin < InputPlugin
|
7
|
-
::Embulk::Plugin.register_input("zendesk", self)
|
8
|
-
|
9
|
-
def self.transaction(config, &control)
|
10
|
-
task = config_to_task(config)
|
11
|
-
client = Client.new(task)
|
12
|
-
client.validate_config
|
13
|
-
|
14
|
-
columns = task[:schema].map do |column|
|
15
|
-
name = column["name"]
|
16
|
-
type = column["type"].to_sym
|
17
|
-
|
18
|
-
Column.new(nil, name, type, column["format"])
|
19
|
-
end
|
20
|
-
|
21
|
-
if task[:incremental] && !Client::AVAILABLE_INCREMENTAL_EXPORT.include?(task[:target])
|
22
|
-
Embulk.logger.warn "target: #{task[:target]} don't support incremental export API. Will be ignored start_time option"
|
23
|
-
end
|
24
|
-
|
25
|
-
resume(task, columns, 1, &control)
|
26
|
-
end
|
27
|
-
|
28
|
-
def self.resume(task, columns, count, &control)
|
29
|
-
task_reports = yield(task, columns, count)
|
30
|
-
report = task_reports.first
|
31
|
-
|
32
|
-
next_config_diff = {}
|
33
|
-
if report[:start_time]
|
34
|
-
next_config_diff[:start_time] = report[:start_time]
|
35
|
-
end
|
36
|
-
return next_config_diff
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.guess(config)
|
40
|
-
task = config_to_task(config)
|
41
|
-
client = Client.new(task)
|
42
|
-
client.validate_config
|
43
|
-
|
44
|
-
records = []
|
45
|
-
client.public_send(task[:target]) do |record|
|
46
|
-
records << record
|
47
|
-
end
|
48
|
-
|
49
|
-
columns = Guess::SchemaGuess.from_hash_records(records).map do |column|
|
50
|
-
hash = column.to_h
|
51
|
-
hash.delete(:index)
|
52
|
-
hash.delete(:format) unless hash[:format]
|
53
|
-
|
54
|
-
# NOTE: Embulk 0.8.1 guesses Hash and Hashes in Array as string.
|
55
|
-
# https://github.com/embulk/embulk/issues/379
|
56
|
-
# This is workaround for that
|
57
|
-
if records.any? {|r| [Array, Hash].include?(r[hash[:name]].class) }
|
58
|
-
hash[:type] = :json
|
59
|
-
end
|
60
|
-
|
61
|
-
case hash[:name]
|
62
|
-
when /_id$/
|
63
|
-
# NOTE: sometimes *_id will be guessed as timestamp format:%d%m%Y (e.g. 21031998), all *_id columns should be type:string
|
64
|
-
hash[:type] = :string
|
65
|
-
hash.delete(:format) # has it if type:timestamp
|
66
|
-
when "id"
|
67
|
-
hash[:type] = :long
|
68
|
-
hash.delete(:format) # has it if type:timestamp
|
69
|
-
end
|
70
|
-
|
71
|
-
hash
|
72
|
-
end
|
73
|
-
|
74
|
-
task[:includes].each do |ent|
|
75
|
-
columns << {
|
76
|
-
name: ent,
|
77
|
-
type: :json
|
78
|
-
}
|
79
|
-
end
|
80
|
-
|
81
|
-
return {"columns" => columns.compact}
|
82
|
-
end
|
83
|
-
|
84
|
-
def self.config_to_task(config)
|
85
|
-
{
|
86
|
-
login_url: config.param("login_url", :string),
|
87
|
-
auth_method: config.param("auth_method", :string, default: "basic"),
|
88
|
-
target: config.param("target", :string),
|
89
|
-
username: config.param("username", :string, default: nil),
|
90
|
-
password: config.param("password", :string, default: nil),
|
91
|
-
token: config.param("token", :string, default: nil),
|
92
|
-
access_token: config.param("access_token", :string, default: nil),
|
93
|
-
start_time: config.param("start_time", :string, default: nil),
|
94
|
-
retry_limit: config.param("retry_limit", :integer, default: 5),
|
95
|
-
retry_initial_wait_sec: config.param("retry_initial_wait_sec", :integer, default: 4),
|
96
|
-
incremental: config.param("incremental", :bool, default: true),
|
97
|
-
dedup: config.param("dedup", :bool, default: true),
|
98
|
-
schema: config.param(:columns, :array, default: []),
|
99
|
-
includes: config.param(:includes, :array, default: []),
|
100
|
-
app_marketplace_integration_name: config.param("app_marketplace_integration_name", :string, default: nil),
|
101
|
-
app_marketplace_org_id: config.param("app_marketplace_org_id", :string, default: nil),
|
102
|
-
app_marketplace_app_id: config.param("app_marketplace_app_id", :string, default: nil)
|
103
|
-
}
|
104
|
-
end
|
105
|
-
|
106
|
-
def init
|
107
|
-
@start_time = Time.parse(task[:start_time]) if task[:start_time]
|
108
|
-
end
|
109
|
-
|
110
|
-
def run
|
111
|
-
method = task[:target]
|
112
|
-
args = [preview?]
|
113
|
-
args << (@start_time || 0).to_i
|
114
|
-
|
115
|
-
# de-dup may lead to OOM
|
116
|
-
if !task[:dedup].nil? && !task[:dedup]
|
117
|
-
args << false
|
118
|
-
end
|
119
|
-
|
120
|
-
mutex = Mutex.new
|
121
|
-
fetching_start_at = Time.now
|
122
|
-
last_data = client.public_send(method, *args) do |record|
|
123
|
-
record = fetch_related_object(record)
|
124
|
-
values = extract_values(record)
|
125
|
-
mutex.synchronize do
|
126
|
-
page_builder.add(values)
|
127
|
-
end
|
128
|
-
break if preview? # NOTE: preview take care only 1 record. subresources fetching is slow.
|
129
|
-
end
|
130
|
-
page_builder.finish
|
131
|
-
|
132
|
-
task_report = {}
|
133
|
-
if task[:incremental]
|
134
|
-
if last_data && last_data["end_time"]
|
135
|
-
# NOTE: start_time compared as "=>", not ">".
|
136
|
-
# If we will use end_time for next start_time, we got the same record that is last fetched
|
137
|
-
# end_time + 1 is workaround for that
|
138
|
-
next_start_time = Time.at(last_data["end_time"] + 1)
|
139
|
-
task_report[:start_time] = next_start_time.strftime("%Y-%m-%d %H:%M:%S%z")
|
140
|
-
else
|
141
|
-
# Sometimes no record and no end_time fetched on the job, but we should generate start_time on config_diff.
|
142
|
-
task_report[:start_time] = fetching_start_at
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
task_report
|
147
|
-
end
|
148
|
-
|
149
|
-
private
|
150
|
-
|
151
|
-
def fetch_related_object(record)
|
152
|
-
return record unless task[:includes] && !task[:includes].empty?
|
153
|
-
task[:includes].each do |ent|
|
154
|
-
record[ent] = client.fetch_subresource(record['id'], task[:target], ent)
|
155
|
-
end
|
156
|
-
record
|
157
|
-
end
|
158
|
-
|
159
|
-
def client
|
160
|
-
Client.new(task)
|
161
|
-
end
|
162
|
-
|
163
|
-
def preview?
|
164
|
-
org.embulk.spi.Exec.isPreview()
|
165
|
-
rescue java.lang.NullPointerException => e
|
166
|
-
false
|
167
|
-
end
|
168
|
-
|
169
|
-
def extract_values(record)
|
170
|
-
values = task[:schema].map do |column|
|
171
|
-
value = record[column["name"].to_s]
|
172
|
-
next if value.nil?
|
173
|
-
cast(value, column["type"].to_s)
|
174
|
-
end
|
175
|
-
|
176
|
-
values
|
177
|
-
end
|
178
|
-
|
179
|
-
def cast(value, type)
|
180
|
-
case type
|
181
|
-
when "timestamp"
|
182
|
-
Time.parse(value)
|
183
|
-
when "double"
|
184
|
-
Float(value)
|
185
|
-
when "long"
|
186
|
-
Integer(value)
|
187
|
-
when "boolean"
|
188
|
-
!!value
|
189
|
-
when "string"
|
190
|
-
value.to_s
|
191
|
-
else
|
192
|
-
value
|
193
|
-
end
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|
data/test/capture_io.rb
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
module CaptureIo
|
2
|
-
def capture(output = :out, &block)
|
3
|
-
_, out = swap_io(output, &block)
|
4
|
-
out
|
5
|
-
end
|
6
|
-
|
7
|
-
def silence(&block)
|
8
|
-
block_result = nil
|
9
|
-
swap_io(:out) do
|
10
|
-
block_result,_ = swap_io(:err, &block)
|
11
|
-
end
|
12
|
-
block_result
|
13
|
-
end
|
14
|
-
|
15
|
-
def swap_io(output = :out, &block)
|
16
|
-
java_import 'java.io.PrintStream'
|
17
|
-
java_import 'java.io.ByteArrayOutputStream'
|
18
|
-
java_import 'java.lang.System'
|
19
|
-
|
20
|
-
ruby_original_stream = output == :out ? $stdout.dup : $stderr.dup
|
21
|
-
java_original_stream = System.send(output) # :out or :err
|
22
|
-
ruby_buf = StringIO.new
|
23
|
-
java_buf = ByteArrayOutputStream.new
|
24
|
-
|
25
|
-
case output
|
26
|
-
when :out
|
27
|
-
$stdout = ruby_buf
|
28
|
-
System.setOut(PrintStream.new(java_buf))
|
29
|
-
when :err
|
30
|
-
$stderr = ruby_buf
|
31
|
-
System.setErr(PrintStream.new(java_buf))
|
32
|
-
end
|
33
|
-
|
34
|
-
[block.call, ruby_buf.string + java_buf.toString]
|
35
|
-
ensure
|
36
|
-
case output
|
37
|
-
when :out
|
38
|
-
$stdout = ruby_original_stream
|
39
|
-
System.setOut(java_original_stream)
|
40
|
-
when :err
|
41
|
-
$stderr = ruby_original_stream
|
42
|
-
System.setErr(java_original_stream)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,722 +0,0 @@
|
|
1
|
-
require "embulk"
|
2
|
-
Embulk.setup
|
3
|
-
|
4
|
-
require "set"
|
5
|
-
require "yaml"
|
6
|
-
require "embulk/input/zendesk"
|
7
|
-
require "override_assert_raise"
|
8
|
-
require "fixture_helper"
|
9
|
-
require "capture_io"
|
10
|
-
require "concurrent/atomic/atomic_fixnum"
|
11
|
-
module Embulk
|
12
|
-
module Input
|
13
|
-
module Zendesk
|
14
|
-
class TestClient < Test::Unit::TestCase
|
15
|
-
include OverrideAssertRaise
|
16
|
-
include FixtureHelper
|
17
|
-
include CaptureIo
|
18
|
-
|
19
|
-
sub_test_case "tickets (incremental export)" do
|
20
|
-
sub_test_case "partial" do
|
21
|
-
def client
|
22
|
-
@client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 1, retry_initial_wait_sec: 0)
|
23
|
-
end
|
24
|
-
|
25
|
-
setup do
|
26
|
-
stub(Embulk).logger { Logger.new(File::NULL) }
|
27
|
-
@httpclient = client.httpclient
|
28
|
-
stub(client).httpclient { @httpclient }
|
29
|
-
end
|
30
|
-
|
31
|
-
test "fetch tickets" do
|
32
|
-
tickets = [
|
33
|
-
{"id" => 1},
|
34
|
-
{"id" => 2},
|
35
|
-
]
|
36
|
-
@httpclient.test_loopback_http_response << [
|
37
|
-
"HTTP/1.1 200",
|
38
|
-
"Content-Type: application/json",
|
39
|
-
"",
|
40
|
-
{
|
41
|
-
tickets: tickets
|
42
|
-
}.to_json
|
43
|
-
].join("\r\n")
|
44
|
-
|
45
|
-
handler = proc { }
|
46
|
-
tickets.each do |ticket|
|
47
|
-
mock(handler).call(ticket)
|
48
|
-
end
|
49
|
-
client.tickets(&handler)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
sub_test_case "ticket_metrics incremental export" do
|
54
|
-
def client
|
55
|
-
@client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 1, retry_initial_wait_sec: 0)
|
56
|
-
end
|
57
|
-
setup do
|
58
|
-
stub(Embulk).logger { Logger.new(File::NULL) }
|
59
|
-
@httpclient = client.httpclient
|
60
|
-
stub(client).httpclient { @httpclient }
|
61
|
-
end
|
62
|
-
test "fetch ticket_metrics with start_time set" do
|
63
|
-
records = 100.times.map{|n| {"id"=> n, "ticket_id"=>n+1}}
|
64
|
-
start_time = 1488535542
|
65
|
-
@httpclient.test_loopback_http_response << [
|
66
|
-
"HTTP/1.1 200",
|
67
|
-
"Content-Type: application/json",
|
68
|
-
"",
|
69
|
-
{
|
70
|
-
metric_sets: records,
|
71
|
-
count: records.size,
|
72
|
-
next_page: nil,
|
73
|
-
}.to_json
|
74
|
-
].join("\r\n")
|
75
|
-
# lock = Mutex.new
|
76
|
-
result_array = records
|
77
|
-
counter=Concurrent::AtomicFixnum.new(0)
|
78
|
-
handler = proc { |record|
|
79
|
-
assert_include(result_array,record)
|
80
|
-
counter.increment
|
81
|
-
}
|
82
|
-
proxy(@httpclient).get("#{login_url}/api/v2/incremental/tickets.json", {:include => "metric_sets", :start_time => start_time}, anything)
|
83
|
-
client.ticket_metrics(false, start_time, &handler)
|
84
|
-
assert_equal(counter.value, result_array.size)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
sub_test_case "ticket_metrics (non-incremental export)" do
|
88
|
-
sub_test_case "partial" do
|
89
|
-
def client
|
90
|
-
@client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 1, retry_initial_wait_sec: 0)
|
91
|
-
end
|
92
|
-
|
93
|
-
setup do
|
94
|
-
stub(Embulk).logger { Logger.new(File::NULL) }
|
95
|
-
@httpclient = client.httpclient
|
96
|
-
stub(client).httpclient { @httpclient }
|
97
|
-
end
|
98
|
-
|
99
|
-
test "fetch ticket_metrics first page only" do
|
100
|
-
records = [
|
101
|
-
{"id" => 1},
|
102
|
-
{"id" => 2},
|
103
|
-
]
|
104
|
-
@httpclient.test_loopback_http_response << [
|
105
|
-
"HTTP/1.1 200",
|
106
|
-
"Content-Type: application/json",
|
107
|
-
"",
|
108
|
-
{
|
109
|
-
ticket_metrics: records,
|
110
|
-
next_page: "https://treasuredata.zendesk.com/api/v2/incremental/tickets.json?include=metric_sets&start_time=1488535542",
|
111
|
-
}.to_json
|
112
|
-
].join("\r\n")
|
113
|
-
|
114
|
-
handler = proc { }
|
115
|
-
records.each do |record|
|
116
|
-
mock(handler).call(record)
|
117
|
-
end
|
118
|
-
client.ticket_metrics(true, &handler)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
sub_test_case "all" do
|
123
|
-
def client
|
124
|
-
@client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 1, retry_initial_wait_sec: 0)
|
125
|
-
end
|
126
|
-
|
127
|
-
setup do
|
128
|
-
stub(Embulk).logger { Logger.new(File::NULL) }
|
129
|
-
@httpclient = client.httpclient
|
130
|
-
stub(client).httpclient { @httpclient }
|
131
|
-
end
|
132
|
-
|
133
|
-
test "fetch ticket_metrics all page" do
|
134
|
-
records = 100.times.map{|n| {"id"=> n, "ticket_id"=>n+1}}
|
135
|
-
second_results = [
|
136
|
-
{"id" => 101, "ticket_id" => 101}
|
137
|
-
]
|
138
|
-
end_time = 1488535542
|
139
|
-
@httpclient.test_loopback_http_response << [
|
140
|
-
"HTTP/1.1 200",
|
141
|
-
"Content-Type: application/json",
|
142
|
-
"",
|
143
|
-
{
|
144
|
-
metric_sets: records,
|
145
|
-
count: 1000,
|
146
|
-
next_page: "#{login_url}/api/v2/incremental/tickets.json?include=metric_sets&start_time=1488535542",
|
147
|
-
end_time: end_time
|
148
|
-
}.to_json
|
149
|
-
].join("\r\n")
|
150
|
-
|
151
|
-
@httpclient.test_loopback_http_response << [
|
152
|
-
"HTTP/1.1 200",
|
153
|
-
"Content-Type: application/json",
|
154
|
-
"",
|
155
|
-
{
|
156
|
-
metric_sets: second_results,
|
157
|
-
count: second_results.size,
|
158
|
-
next_page: nil,
|
159
|
-
}.to_json
|
160
|
-
].join("\r\n")
|
161
|
-
# lock = Mutex.new
|
162
|
-
result_array = records + second_results
|
163
|
-
counter=Concurrent::AtomicFixnum.new(0)
|
164
|
-
handler = proc { |record|
|
165
|
-
assert_include(result_array,record)
|
166
|
-
counter.increment
|
167
|
-
}
|
168
|
-
|
169
|
-
proxy(@httpclient).get("#{login_url}/api/v2/incremental/tickets.json", {:include => "metric_sets", :start_time => 0}, anything)
|
170
|
-
proxy(@httpclient).get("#{login_url}/api/v2/incremental/tickets.json", {:include => "metric_sets",:start_time => end_time}, anything)
|
171
|
-
|
172
|
-
client.ticket_metrics(false, &handler)
|
173
|
-
assert_equal(counter.value, result_array.size)
|
174
|
-
end
|
175
|
-
|
176
|
-
test "fetch tickets metrics without duplicated" do
|
177
|
-
records = [
|
178
|
-
{"id" => 1, "ticket_id" => 100},
|
179
|
-
{"id" => 2, "ticket_id" => 200},
|
180
|
-
{"id" => 1, "ticket_id" => 100},
|
181
|
-
{"id" => 1, "ticket_id" => 100},
|
182
|
-
]
|
183
|
-
@httpclient.test_loopback_http_response << [
|
184
|
-
"HTTP/1.1 200",
|
185
|
-
"Content-Type: application/json",
|
186
|
-
"",
|
187
|
-
{
|
188
|
-
metric_sets: records,
|
189
|
-
count: records.length,
|
190
|
-
}.to_json
|
191
|
-
].join("\r\n")
|
192
|
-
counter = Concurrent::AtomicFixnum.new(0)
|
193
|
-
handler = proc {counter.increment}
|
194
|
-
client.ticket_metrics(false, &handler)
|
195
|
-
assert_equal(2,counter.value)
|
196
|
-
end
|
197
|
-
|
198
|
-
test "allows to fetch tickets metrics *with* duplicated" do
|
199
|
-
records = [
|
200
|
-
{"id" => 1, "ticket_id" => 100},
|
201
|
-
{"id" => 2, "ticket_id" => 200},
|
202
|
-
{"id" => 1, "ticket_id" => 100},
|
203
|
-
{"id" => 1, "ticket_id" => 100},
|
204
|
-
]
|
205
|
-
@httpclient.test_loopback_http_response << [
|
206
|
-
"HTTP/1.1 200",
|
207
|
-
"Content-Type: application/json",
|
208
|
-
"",
|
209
|
-
{
|
210
|
-
metric_sets: records,
|
211
|
-
count: records.length,
|
212
|
-
}.to_json
|
213
|
-
].join("\r\n")
|
214
|
-
counter = Concurrent::AtomicFixnum.new(0)
|
215
|
-
handler = proc {counter.increment}
|
216
|
-
client.ticket_metrics(false, 0, false, &handler)
|
217
|
-
assert_equal(4,counter.value)
|
218
|
-
end
|
219
|
-
|
220
|
-
test "fetch ticket_metrics with next_page" do
|
221
|
-
end_time = 1488535542
|
222
|
-
response_1 = [
|
223
|
-
"HTTP/1.1 200",
|
224
|
-
"Content-Type: application/json",
|
225
|
-
"",
|
226
|
-
{
|
227
|
-
metric_sets: 100.times.map{|n| {"id" => n, "ticket_id" => n+1}},
|
228
|
-
count: 1001,
|
229
|
-
end_time: end_time,
|
230
|
-
next_page: "#{login_url}/api/v2/incremental/tickets.json?include=metric_sets&start_time=1488535542",
|
231
|
-
}.to_json
|
232
|
-
].join("\r\n")
|
233
|
-
|
234
|
-
response_2 = [
|
235
|
-
"HTTP/1.1 200",
|
236
|
-
"Content-Type: application/json",
|
237
|
-
"",
|
238
|
-
{
|
239
|
-
metric_sets: [{"id" => 101, "ticket_id" => 101}],
|
240
|
-
count: 101,
|
241
|
-
}.to_json
|
242
|
-
].join("\r\n")
|
243
|
-
|
244
|
-
|
245
|
-
@httpclient.test_loopback_http_response << response_1
|
246
|
-
@httpclient.test_loopback_http_response << response_2
|
247
|
-
counter = Concurrent::AtomicFixnum.new(0)
|
248
|
-
handler = proc { counter.increment }
|
249
|
-
proxy(@httpclient).get("#{login_url}/api/v2/incremental/tickets.json",{:include=>"metric_sets", :start_time=>0},anything)
|
250
|
-
proxy(@httpclient).get("#{login_url}/api/v2/incremental/tickets.json",{:include=>"metric_sets", :start_time=>end_time},anything)
|
251
|
-
client.ticket_metrics(false, &handler)
|
252
|
-
assert_equal(101, counter.value)
|
253
|
-
end
|
254
|
-
|
255
|
-
test "raise DataError when invalid JSON response" do
|
256
|
-
@httpclient.test_loopback_http_response << [
|
257
|
-
"HTTP/1.1 200",
|
258
|
-
"Content-Type: application/json",
|
259
|
-
"",
|
260
|
-
"[[[" # invalid json
|
261
|
-
].join("\r\n")
|
262
|
-
|
263
|
-
assert_raise(DataError) do
|
264
|
-
client.tickets(false)
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
end
|
269
|
-
|
270
|
-
sub_test_case "targets" do
|
271
|
-
def client
|
272
|
-
@client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 1, retry_initial_wait_sec: 0)
|
273
|
-
end
|
274
|
-
|
275
|
-
setup do
|
276
|
-
stub(Embulk).logger { Logger.new(File::NULL) }
|
277
|
-
@httpclient = client.httpclient
|
278
|
-
stub(client).httpclient { @httpclient }
|
279
|
-
end
|
280
|
-
|
281
|
-
sub_test_case "ticket_events" do
|
282
|
-
test "invoke incremental_export when partial=true" do
|
283
|
-
mock(client).incremental_export(anything, "ticket_events", anything, true, Set.new, true)
|
284
|
-
client.ticket_events(true)
|
285
|
-
end
|
286
|
-
|
287
|
-
test "invoke incremental_export when partial=false" do
|
288
|
-
mock(client).incremental_export(anything, "ticket_events", anything, true, Set.new, false)
|
289
|
-
client.ticket_events(false)
|
290
|
-
end
|
291
|
-
end
|
292
|
-
|
293
|
-
sub_test_case "ticket_fields" do
|
294
|
-
test "invoke export when partial=true" do
|
295
|
-
mock(client).export(anything, "ticket_fields")
|
296
|
-
client.ticket_fields(true)
|
297
|
-
end
|
298
|
-
|
299
|
-
test "invoke export when partial=false" do
|
300
|
-
# Added default `start_time`
|
301
|
-
mock(client).export_parallel(anything, "ticket_fields", 0, true, false) # new args: `dedup`, `paging`
|
302
|
-
client.ticket_fields(false)
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
sub_test_case "ticket_forms" do
|
307
|
-
test "invoke export when partial=true" do
|
308
|
-
mock(client).export(anything, "ticket_forms")
|
309
|
-
client.ticket_forms(true)
|
310
|
-
end
|
311
|
-
|
312
|
-
test "invoke export when partial=false" do
|
313
|
-
# Added default `start_time`
|
314
|
-
mock(client).export_parallel(anything, "ticket_forms", 0, true, false) # new args: `dedup`, `paging`)
|
315
|
-
client.ticket_forms(false)
|
316
|
-
end
|
317
|
-
end
|
318
|
-
|
319
|
-
sub_test_case "no pagination" do
|
320
|
-
data("ticket_fields", "ticket_fields")
|
321
|
-
data("ticket_forms", "ticket_forms")
|
322
|
-
test "non-incremental targets" do |target|
|
323
|
-
response = [
|
324
|
-
"HTTP/1.1 200",
|
325
|
-
"Content-Type: application/json",
|
326
|
-
"",
|
327
|
-
{
|
328
|
-
target => 200.times.map{|n| {"id" => n}},
|
329
|
-
count: 200,
|
330
|
-
next_page: nil,
|
331
|
-
previous_page: nil,
|
332
|
-
}.to_json
|
333
|
-
].join("\r\n")
|
334
|
-
|
335
|
-
# mock multiple responses, to simulate real API behavior
|
336
|
-
@httpclient.test_loopback_http_response << response
|
337
|
-
@httpclient.test_loopback_http_response << response
|
338
|
-
counter = Concurrent::AtomicFixnum.new(0)
|
339
|
-
handler = proc { counter.increment }
|
340
|
-
# validate expected target
|
341
|
-
proxy(@httpclient).get("#{login_url}/api/v2/#{target}.json",anything,anything)
|
342
|
-
# (`partial`, `start_time`, `default`, `block`)
|
343
|
-
client.public_send(target, false, 0, true, &handler)
|
344
|
-
# only ingest 200 records
|
345
|
-
assert_equal(200, counter.value)
|
346
|
-
end
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
|
-
|
351
|
-
sub_test_case "auth" do
|
352
|
-
test "httpclient call validate_credentials" do
|
353
|
-
client = Client.new({})
|
354
|
-
mock(client).validate_credentials
|
355
|
-
client.httpclient
|
356
|
-
end
|
357
|
-
|
358
|
-
sub_test_case "auth_method: basic" do
|
359
|
-
test "don't raise on validate when username and password given" do
|
360
|
-
client = Client.new(login_url: login_url, auth_method: "basic", username: username, password: password)
|
361
|
-
assert_nothing_raised do
|
362
|
-
client.validate_credentials
|
363
|
-
end
|
364
|
-
|
365
|
-
any_instance_of(HTTPClient) do |klass|
|
366
|
-
mock(klass).set_auth(login_url, username, password)
|
367
|
-
end
|
368
|
-
client.httpclient
|
369
|
-
end
|
370
|
-
|
371
|
-
test "set_auth called with valid credential" do
|
372
|
-
client = Client.new(login_url: login_url, auth_method: "basic", username: username, password: password)
|
373
|
-
|
374
|
-
any_instance_of(HTTPClient) do |klass|
|
375
|
-
mock(klass).set_auth(login_url, username, password)
|
376
|
-
end
|
377
|
-
client.httpclient
|
378
|
-
end
|
379
|
-
|
380
|
-
data do
|
381
|
-
[
|
382
|
-
["username", {username: "foo@example.com"}],
|
383
|
-
["password", {password: "passWORD"}],
|
384
|
-
["nothing both", {}],
|
385
|
-
]
|
386
|
-
end
|
387
|
-
test "username only given" do |config|
|
388
|
-
client = Client.new(config.merge(auth_method: "basic"))
|
389
|
-
assert_raise(ConfigError) do
|
390
|
-
client.validate_credentials
|
391
|
-
end
|
392
|
-
end
|
393
|
-
end
|
394
|
-
|
395
|
-
sub_test_case "auth_method: token" do
|
396
|
-
test "don't raise on validate when username and token given" do
|
397
|
-
client = Client.new(login_url: login_url, auth_method: "token", username: username, token: token)
|
398
|
-
assert_nothing_raised do
|
399
|
-
client.validate_credentials
|
400
|
-
end
|
401
|
-
end
|
402
|
-
|
403
|
-
test "set_auth called with valid credential" do
|
404
|
-
client = Client.new(login_url: login_url, auth_method: "token", username: username, token: token)
|
405
|
-
|
406
|
-
any_instance_of(HTTPClient) do |klass|
|
407
|
-
mock(klass).set_auth(login_url, "#{username}/token", token)
|
408
|
-
end
|
409
|
-
client.httpclient
|
410
|
-
end
|
411
|
-
|
412
|
-
data do
|
413
|
-
[
|
414
|
-
["username", {username: "foo@example.com"}],
|
415
|
-
["token", {token: "TOKEN"}],
|
416
|
-
["nothing both", {}],
|
417
|
-
]
|
418
|
-
end
|
419
|
-
test "username only given" do |config|
|
420
|
-
client = Client.new(config.merge(auth_method: "token"))
|
421
|
-
assert_raise(ConfigError) do
|
422
|
-
client.validate_credentials
|
423
|
-
end
|
424
|
-
end
|
425
|
-
end
|
426
|
-
|
427
|
-
sub_test_case "auth_method: oauth" do
|
428
|
-
test "don't raise on validate when access_token given" do
|
429
|
-
client = Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token)
|
430
|
-
assert_nothing_raised do
|
431
|
-
client.validate_credentials
|
432
|
-
end
|
433
|
-
end
|
434
|
-
|
435
|
-
test "set default header with valid credential" do
|
436
|
-
client = Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token)
|
437
|
-
|
438
|
-
any_instance_of(HTTPClient) do |klass|
|
439
|
-
mock(klass).default_header = {
|
440
|
-
"Authorization" => "Bearer #{access_token}"
|
441
|
-
}
|
442
|
-
end
|
443
|
-
client.httpclient
|
444
|
-
end
|
445
|
-
|
446
|
-
test "access_token not given" do |config|
|
447
|
-
client = Client.new(auth_method: "oauth")
|
448
|
-
assert_raise(ConfigError) do
|
449
|
-
client.validate_credentials
|
450
|
-
end
|
451
|
-
end
|
452
|
-
end
|
453
|
-
|
454
|
-
sub_test_case "auth_method: unknown" do
|
455
|
-
test "raise on validate" do
|
456
|
-
client = Client.new(auth_method: "unknown")
|
457
|
-
assert_raise(ConfigError) do
|
458
|
-
client.validate_credentials
|
459
|
-
end
|
460
|
-
end
|
461
|
-
end
|
462
|
-
end
|
463
|
-
|
464
|
-
sub_test_case "retry" do
|
465
|
-
def client
|
466
|
-
@client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 2, retry_initial_wait_sec: 0)
|
467
|
-
end
|
468
|
-
|
469
|
-
def stub_response(status, headers = [], body_json = nil)
|
470
|
-
headers << "Content-Type: application/json"
|
471
|
-
@httpclient.test_loopback_http_response << [
|
472
|
-
"HTTP/1.1 #{status}",
|
473
|
-
headers.join("\r\n"),
|
474
|
-
"",
|
475
|
-
body_json || {
|
476
|
-
tickets: []
|
477
|
-
}.to_json
|
478
|
-
].join("\r\n")
|
479
|
-
end
|
480
|
-
|
481
|
-
setup do
|
482
|
-
retryer = PerfectRetry.new do |conf|
|
483
|
-
conf.dont_rescues = [Exception] # Don't care any exceptions to retry
|
484
|
-
end
|
485
|
-
|
486
|
-
stub(Embulk).logger { Logger.new(File::NULL) }
|
487
|
-
@httpclient = client.httpclient
|
488
|
-
stub(client).httpclient { @httpclient }
|
489
|
-
stub(client).retryer { retryer }
|
490
|
-
PerfectRetry.disable!
|
491
|
-
end
|
492
|
-
|
493
|
-
teardown do
|
494
|
-
PerfectRetry.enable!
|
495
|
-
end
|
496
|
-
|
497
|
-
test "400" do
|
498
|
-
stub_response(400)
|
499
|
-
assert_raise(ConfigError) do
|
500
|
-
client.tickets(&proc{})
|
501
|
-
end
|
502
|
-
end
|
503
|
-
|
504
|
-
test "403 forbidden" do
|
505
|
-
stub_response(403)
|
506
|
-
assert_raise(ConfigError) do
|
507
|
-
client.tickets(&proc{})
|
508
|
-
end
|
509
|
-
end
|
510
|
-
|
511
|
-
test "409" do
|
512
|
-
stub_response(409)
|
513
|
-
assert_raise(StandardError) do
|
514
|
-
client.tickets(&proc{})
|
515
|
-
end
|
516
|
-
end
|
517
|
-
|
518
|
-
test "422 with Too recent start_time" do
|
519
|
-
stub_response(422, [], '{"error":"InvalidValue","description":"Too recent start_time. Use a start_time older than 5 minutes"}')
|
520
|
-
assert_nothing_raised do
|
521
|
-
client.tickets(&proc{})
|
522
|
-
end
|
523
|
-
end
|
524
|
-
|
525
|
-
test "422" do
|
526
|
-
stub_response(422)
|
527
|
-
assert_raise(ConfigError) do
|
528
|
-
client.tickets(&proc{})
|
529
|
-
end
|
530
|
-
end
|
531
|
-
|
532
|
-
test "429" do
|
533
|
-
after = "123"
|
534
|
-
stub_response(429, ["Retry-After: #{after}"])
|
535
|
-
mock(client).sleep after.to_i
|
536
|
-
assert_throw(:retry) do
|
537
|
-
client.tickets(false, &proc{})
|
538
|
-
end
|
539
|
-
end
|
540
|
-
|
541
|
-
test "429 guess/preview fail fast" do
|
542
|
-
after = "123"
|
543
|
-
stub_response(429, ["Retry-After: #{after}"])
|
544
|
-
assert_raise(DataError.new("Rate Limited. Waiting #{after} seconds to re-run")) do
|
545
|
-
client.tickets(&proc{})
|
546
|
-
end
|
547
|
-
end
|
548
|
-
|
549
|
-
test "500" do
|
550
|
-
stub_response(500)
|
551
|
-
assert_raise(StandardError) do
|
552
|
-
client.tickets(&proc{})
|
553
|
-
end
|
554
|
-
end
|
555
|
-
|
556
|
-
test "503" do
|
557
|
-
stub_response(503)
|
558
|
-
assert_raise(StandardError) do
|
559
|
-
client.tickets(&proc{})
|
560
|
-
end
|
561
|
-
end
|
562
|
-
|
563
|
-
test "503 with Retry-After" do
|
564
|
-
after = "123"
|
565
|
-
stub_response(503, ["Retry-After: #{after}"])
|
566
|
-
mock(client).sleep after.to_i
|
567
|
-
assert_throw(:retry) do
|
568
|
-
client.tickets(false, &proc{})
|
569
|
-
end
|
570
|
-
end
|
571
|
-
|
572
|
-
test "503 with Retry-After guess/preview fail fast" do
|
573
|
-
after = "123"
|
574
|
-
stub_response(503, ["Retry-After: #{after}"])
|
575
|
-
assert_raise(DataError.new("Rate Limited. Waiting #{after} seconds to re-run")) do
|
576
|
-
client.tickets(&proc{})
|
577
|
-
end
|
578
|
-
end
|
579
|
-
|
580
|
-
test "Unhandled response code (555)" do
|
581
|
-
error_body = {error: "FATAL ERROR"}.to_json
|
582
|
-
stub_response(555, [], error_body)
|
583
|
-
assert_raise(RuntimeError.new("Server returns unknown status code (555) #{error_body}")) do
|
584
|
-
client.tickets(&proc{})
|
585
|
-
end
|
586
|
-
end
|
587
|
-
end
|
588
|
-
|
589
|
-
sub_test_case ".validate_target" do
|
590
|
-
data do
|
591
|
-
[
|
592
|
-
["tickets", ["tickets", nil]],
|
593
|
-
["ticket_events", ["ticket_events", nil]],
|
594
|
-
["users", ["users", nil]],
|
595
|
-
["organizations", ["organizations", nil]],
|
596
|
-
["unknown", ["unknown", Embulk::ConfigError]],
|
597
|
-
]
|
598
|
-
end
|
599
|
-
test "validate with target" do |data|
|
600
|
-
target, error = data
|
601
|
-
client = Client.new({target: target})
|
602
|
-
|
603
|
-
if error
|
604
|
-
assert_raise(error) do
|
605
|
-
client.validate_target
|
606
|
-
end
|
607
|
-
else
|
608
|
-
assert_nothing_raised do
|
609
|
-
client.validate_target
|
610
|
-
end
|
611
|
-
end
|
612
|
-
end
|
613
|
-
end
|
614
|
-
|
615
|
-
sub_test_case ".extract_valid_json_from_chunk" do
|
616
|
-
setup do
|
617
|
-
@client = Client.new({target: "tickets"})
|
618
|
-
end
|
619
|
-
|
620
|
-
test "complete json" do
|
621
|
-
actual = @client.send(:extract_valid_json_from_chunk, '{"tickets":[{"foo":1},{"foo":2}]}')
|
622
|
-
assert_equal ['{"foo":1}', '{"foo":2}'], actual
|
623
|
-
end
|
624
|
-
|
625
|
-
test "broken json" do
|
626
|
-
json = '{"ticket_events":[{"foo":1},{"foo":2},{"fo'
|
627
|
-
actual = @client.send(:extract_valid_json_from_chunk, json)
|
628
|
-
expected = [
|
629
|
-
'{"foo":1}',
|
630
|
-
'{"foo":2}',
|
631
|
-
]
|
632
|
-
assert_equal expected, actual
|
633
|
-
end
|
634
|
-
end
|
635
|
-
|
636
|
-
sub_test_case "should not create new instance of httpclient" do
|
637
|
-
test "not create new instance when re-call" do
|
638
|
-
client = Client.new(login_url: login_url, auth_method: "token", username: username, token: token)
|
639
|
-
assert client.httpclient == client.httpclient
|
640
|
-
end
|
641
|
-
end
|
642
|
-
|
643
|
-
sub_test_case "ensure thread pool is shutdown with/without errors, retry for TempError" do
|
644
|
-
def client
|
645
|
-
@client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 1, retry_initial_wait_sec: 0)
|
646
|
-
end
|
647
|
-
|
648
|
-
setup do
|
649
|
-
stub(Embulk).logger { Logger.new(File::NULL) }
|
650
|
-
@httpclient = client.httpclient
|
651
|
-
stub(client).httpclient { @httpclient }
|
652
|
-
@pool = Concurrent::ThreadPoolExecutor.new
|
653
|
-
stub(client).create_pool { @pool }
|
654
|
-
end
|
655
|
-
test "should shutdown pool - without error" do
|
656
|
-
@httpclient.test_loopback_http_response << [
|
657
|
-
"HTTP/1.1 200",
|
658
|
-
"Content-Type: application/json",
|
659
|
-
"",
|
660
|
-
{
|
661
|
-
tickets: [{ id: 1 }],
|
662
|
-
count: 1
|
663
|
-
}.to_json
|
664
|
-
].join("\r\n")
|
665
|
-
handler = proc { }
|
666
|
-
client.tickets(false, &handler)
|
667
|
-
assert_equal(true, @pool.shutdown?)
|
668
|
-
end
|
669
|
-
|
670
|
-
test "should shutdown pool - retry TempError and raise DataError" do
|
671
|
-
response = [
|
672
|
-
"HTTP/1.1 200",
|
673
|
-
"Content-Type: application/json",
|
674
|
-
"",
|
675
|
-
{ }.to_json # no required key: `tickets`, raise TempError
|
676
|
-
].join("\r\n")
|
677
|
-
@httpclient.test_loopback_http_response << response
|
678
|
-
@httpclient.test_loopback_http_response << response # retry 1
|
679
|
-
assert_raise(DataError) do
|
680
|
-
client.tickets(false)
|
681
|
-
end
|
682
|
-
assert_equal(true, @pool.shutdown?)
|
683
|
-
end
|
684
|
-
|
685
|
-
test "should shutdown pool - with DataError (no retry)" do
|
686
|
-
response = [
|
687
|
-
"HTTP/1.1 400", # unhandled error, wrapped in DataError
|
688
|
-
"Content-Type: application/json",
|
689
|
-
"",
|
690
|
-
{ }.to_json
|
691
|
-
].join("\r\n")
|
692
|
-
@httpclient.test_loopback_http_response << response
|
693
|
-
assert_raise(DataError) do
|
694
|
-
client.tickets(false)
|
695
|
-
end
|
696
|
-
assert_equal(true, @pool.shutdown?)
|
697
|
-
end
|
698
|
-
end
|
699
|
-
|
700
|
-
def login_url
|
701
|
-
"http://example.com"
|
702
|
-
end
|
703
|
-
|
704
|
-
def username
|
705
|
-
"foo@example.com"
|
706
|
-
end
|
707
|
-
|
708
|
-
def password
|
709
|
-
"passWORD"
|
710
|
-
end
|
711
|
-
|
712
|
-
def token
|
713
|
-
"TOKEN"
|
714
|
-
end
|
715
|
-
|
716
|
-
def access_token
|
717
|
-
"ACCESS_TOKEN"
|
718
|
-
end
|
719
|
-
end
|
720
|
-
end
|
721
|
-
end
|
722
|
-
end
|