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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +9 -3
  3. data/.travis.yml +5 -44
  4. data/CHANGELOG.md +3 -0
  5. data/README.md +5 -5
  6. data/build.gradle +123 -0
  7. data/classpath/commons-codec-1.10.jar +0 -0
  8. data/classpath/commons-logging-1.2.jar +0 -0
  9. data/classpath/embulk-input-zendesk-0.3.0.jar +0 -0
  10. data/classpath/httpclient-4.5.6.jar +0 -0
  11. data/classpath/httpcore-4.4.10.jar +0 -0
  12. data/config/checkstyle/checkstyle.xml +128 -0
  13. data/config/checkstyle/default.xml +108 -0
  14. data/gradle/wrapper/gradle-wrapper.jar +0 -0
  15. data/gradle/wrapper/gradle-wrapper.properties +5 -0
  16. data/gradlew +172 -0
  17. data/gradlew.bat +84 -0
  18. data/lib/embulk/guess/zendesk.rb +21 -0
  19. data/lib/embulk/input/zendesk.rb +3 -9
  20. data/src/main/java/org/embulk/input/zendesk/ZendeskInputPlugin.java +471 -0
  21. data/src/main/java/org/embulk/input/zendesk/clients/ZendeskRestClient.java +268 -0
  22. data/src/main/java/org/embulk/input/zendesk/models/AuthenticationMethod.java +23 -0
  23. data/src/main/java/org/embulk/input/zendesk/models/Target.java +46 -0
  24. data/src/main/java/org/embulk/input/zendesk/models/ZendeskException.java +25 -0
  25. data/src/main/java/org/embulk/input/zendesk/services/ZendeskSupportAPIService.java +109 -0
  26. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskConstants.java +61 -0
  27. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskDateUtils.java +51 -0
  28. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskUtils.java +150 -0
  29. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskValidatorUtils.java +92 -0
  30. data/src/test/java/org/embulk/input/zendesk/TestZendeskInputPlugin.java +232 -0
  31. data/src/test/java/org/embulk/input/zendesk/clients/TestZendeskRestClient.java +351 -0
  32. data/src/test/java/org/embulk/input/zendesk/services/TestZendeskSupportAPIService.java +172 -0
  33. data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskDateUtils.java +36 -0
  34. data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskUtil.java +160 -0
  35. data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskValidatorUtils.java +138 -0
  36. data/src/test/java/org/embulk/input/zendesk/utils/ZendeskPluginTestRuntime.java +133 -0
  37. data/src/test/java/org/embulk/input/zendesk/utils/ZendeskTestHelper.java +63 -0
  38. data/src/test/resources/config/base.yml +14 -0
  39. data/src/test/resources/config/base_validator.yml +48 -0
  40. data/src/test/resources/config/incremental.yml +54 -0
  41. data/src/test/resources/config/non-incremental.yml +39 -0
  42. data/src/test/resources/config/util.yml +18 -0
  43. data/src/test/resources/data/client.json +293 -0
  44. data/src/test/resources/data/error_data.json +187 -0
  45. data/src/test/resources/data/expected/ticket_column.json +148 -0
  46. data/src/test/resources/data/expected/ticket_column_with_related_objects.json +152 -0
  47. data/src/test/resources/data/expected/ticket_fields_column.json +92 -0
  48. data/src/test/resources/data/expected/ticket_metrics_column.json +98 -0
  49. data/src/test/resources/data/ticket_fields.json +225 -0
  50. data/src/test/resources/data/ticket_metrics.json +397 -0
  51. data/src/test/resources/data/ticket_with_related_objects.json +67 -0
  52. data/src/test/resources/data/tickets.json +232 -0
  53. data/src/test/resources/data/tickets_continue.json +52 -0
  54. data/src/test/resources/data/util.json +19 -0
  55. data/src/test/resources/data/util_page.json +227 -0
  56. metadata +65 -221
  57. data/.ruby-version +0 -1
  58. data/.travis.yml.erb +0 -43
  59. data/Gemfile +0 -2
  60. data/Rakefile +0 -21
  61. data/embulk-input-zendesk.gemspec +0 -29
  62. data/gemfiles/embulk-0.8.0-latest +0 -4
  63. data/gemfiles/embulk-0.8.1 +0 -4
  64. data/gemfiles/embulk-latest +0 -4
  65. data/gemfiles/template.erb +0 -4
  66. data/lib/embulk/input/zendesk/client.rb +0 -434
  67. data/lib/embulk/input/zendesk/plugin.rb +0 -199
  68. data/test/capture_io.rb +0 -45
  69. data/test/embulk/input/zendesk/test_client.rb +0 -722
  70. data/test/embulk/input/zendesk/test_plugin.rb +0 -628
  71. data/test/fixture_helper.rb +0 -11
  72. data/test/fixtures/invalid_app_marketplace_lack_one_property.yml +0 -13
  73. data/test/fixtures/invalid_app_marketplace_lack_two_property.yml +0 -12
  74. data/test/fixtures/invalid_lack_username.yml +0 -9
  75. data/test/fixtures/invalid_unknown_auth.yml +0 -9
  76. data/test/fixtures/tickets.json +0 -44
  77. data/test/fixtures/valid_app_marketplace.yml +0 -14
  78. data/test/fixtures/valid_auth_basic.yml +0 -11
  79. data/test/fixtures/valid_auth_oauth.yml +0 -10
  80. data/test/fixtures/valid_auth_token.yml +0 -11
  81. data/test/override_assert_raise.rb +0 -21
  82. 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