embulk-input-zendesk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
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
+ resume(task, columns, 1, &control)
22
+ end
23
+
24
+ def self.resume(task, columns, count, &control)
25
+ task_reports = yield(task, columns, count)
26
+
27
+ next_config_diff = {}
28
+ return next_config_diff
29
+ end
30
+
31
+ def self.guess(config)
32
+ task = config_to_task(config)
33
+ client = Client.new(task)
34
+ client.validate_config
35
+
36
+ records = []
37
+ client.public_send(task[:target]) do |record|
38
+ records << record
39
+ end
40
+
41
+ columns = Guess::SchemaGuess.from_hash_records(records).map do |column|
42
+ hash = column.to_h
43
+ hash.delete(:index)
44
+ hash.delete(:format) unless hash[:format]
45
+
46
+ # NOTE: Embulk 0.8.1 guesses Hash and Hashes in Array as string.
47
+ # https://github.com/embulk/embulk/issues/379
48
+ # This is workaround for that
49
+ if records.any? {|r| [Array, Hash].include?(r[hash[:name]].class) }
50
+ hash[:type] = :json
51
+ end
52
+
53
+ # NOTE: current version don't support JSON type
54
+ next if hash[:type] == :json
55
+
56
+ hash
57
+ end
58
+
59
+ return {"columns" => columns.compact}
60
+ end
61
+
62
+ def self.config_to_task(config)
63
+ {
64
+ login_url: config.param("login_url", :string),
65
+ auth_method: config.param("auth_method", :string, default: "basic"),
66
+ target: config.param("target", :string),
67
+ username: config.param("username", :string, default: nil),
68
+ password: config.param("password", :string, default: nil),
69
+ token: config.param("token", :string, default: nil),
70
+ access_token: config.param("access_token", :string, default: nil),
71
+ start_time: config.param("start_time", :string, default: nil),
72
+ retry_limit: config.param("retry_limit", :integer, default: 5),
73
+ retry_initial_wait_sec: config.param("retry_initial_wait_sec", :integer, default: 1),
74
+ schema: config.param(:columns, :array, default: []),
75
+ }
76
+ end
77
+
78
+ def init
79
+ @start_time = Time.parse(task[:start_time]) if task[:start_time]
80
+ end
81
+
82
+ def run
83
+ method = task[:target]
84
+ args = [preview?]
85
+ if !preview? && @start_time
86
+ args << @start_time.to_i
87
+ end
88
+
89
+ client = Client.new(task)
90
+ client.public_send(method, *args) do |record|
91
+ values = extract_values(record)
92
+ page_builder.add(values)
93
+ end
94
+
95
+ page_builder.finish
96
+
97
+ task_report = {}
98
+ return task_report
99
+ end
100
+
101
+ private
102
+
103
+ def preview?
104
+ org.embulk.spi.Exec.isPreview()
105
+ rescue java.lang.NullPointerException => e
106
+ false
107
+ end
108
+
109
+ def extract_values(record)
110
+ values = task[:schema].map do |column|
111
+ value = record[column["name"].to_s]
112
+ cast(value, column["type"].to_s)
113
+ end
114
+
115
+ values
116
+ end
117
+
118
+ def cast(value, type)
119
+ case type
120
+ when "timestamp"
121
+ Time.parse(value)
122
+ when "double"
123
+ Float(value)
124
+ when "long"
125
+ Integer(value)
126
+ when "boolean"
127
+ !!value
128
+ when "string"
129
+ value.to_s
130
+ else
131
+ value
132
+ end
133
+ end
134
+ end
135
+
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,45 @@
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
@@ -0,0 +1,469 @@
1
+ require "embulk"
2
+ Embulk.setup
3
+
4
+ require "yaml"
5
+ require "embulk/input/zendesk"
6
+ require "override_assert_raise"
7
+ require "fixture_helper"
8
+ require "capture_io"
9
+
10
+ module Embulk
11
+ module Input
12
+ module Zendesk
13
+ class TestClient < Test::Unit::TestCase
14
+ include OverrideAssertRaise
15
+ include FixtureHelper
16
+ include CaptureIo
17
+
18
+ sub_test_case "tickets" do
19
+ sub_test_case "partial" do
20
+ def client
21
+ @client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 1, retry_initial_wait_sec: 0)
22
+ end
23
+
24
+ setup do
25
+ stub(Embulk).logger { Logger.new(File::NULL) }
26
+ @httpclient = client.httpclient
27
+ stub(client).httpclient { @httpclient }
28
+ end
29
+
30
+ test "fetch tickets" do
31
+ tickets = [
32
+ {"id" => 1},
33
+ {"id" => 2},
34
+ ]
35
+ @httpclient.test_loopback_http_response << [
36
+ "HTTP/1.1 200",
37
+ "Content-Type: application/json",
38
+ "",
39
+ {
40
+ tickets: tickets
41
+ }.to_json
42
+ ].join("\r\n")
43
+
44
+ handler = proc { }
45
+ tickets.each do |ticket|
46
+ mock(handler).call(ticket)
47
+ end
48
+ client.tickets(&handler)
49
+ end
50
+
51
+ test "raise DataError when invalid JSON response" do
52
+ @httpclient.test_loopback_http_response << [
53
+ "HTTP/1.1 200",
54
+ "Content-Type: application/json",
55
+ "",
56
+ "[[[" # invalid json
57
+ ].join("\r\n")
58
+
59
+ assert_raise(DataError) do
60
+ client.tickets
61
+ end
62
+ end
63
+ end
64
+
65
+ sub_test_case "all" do
66
+ def client
67
+ @client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 1, retry_initial_wait_sec: 0)
68
+ end
69
+
70
+ setup do
71
+ stub(Embulk).logger { Logger.new(File::NULL) }
72
+ @httpclient = client.httpclient
73
+ stub(client).httpclient { @httpclient }
74
+ end
75
+
76
+ test "fetch tickets" do
77
+ tickets = [
78
+ {"id" => 1},
79
+ {"id" => 2},
80
+ ]
81
+ @httpclient.test_loopback_http_response << [
82
+ "HTTP/1.1 200",
83
+ "Content-Type: application/json",
84
+ "",
85
+ {
86
+ tickets: tickets
87
+ }.to_json
88
+ ].join("\r\n")
89
+
90
+ handler = proc { }
91
+ tickets.each do |ticket|
92
+ mock(handler).call(ticket)
93
+ end
94
+ client.tickets(false, &handler)
95
+ end
96
+
97
+ test "fetch tickets without duplicated" do
98
+ tickets = [
99
+ {"id" => 1},
100
+ {"id" => 2},
101
+ {"id" => 1},
102
+ {"id" => 1},
103
+ ]
104
+ @httpclient.test_loopback_http_response << [
105
+ "HTTP/1.1 200",
106
+ "Content-Type: application/json",
107
+ "",
108
+ {
109
+ tickets: tickets
110
+ }.to_json
111
+ ].join("\r\n")
112
+
113
+ handler = proc { }
114
+ mock(handler).call(anything).twice
115
+ client.tickets(false, &handler)
116
+ end
117
+
118
+ test "fetch tickets with next_page" do
119
+ end_time = Time.now.to_i
120
+
121
+ response_1 = [
122
+ "HTTP/1.1 200",
123
+ "Content-Type: application/json",
124
+ "",
125
+ {
126
+ tickets: [{"id" => 1}],
127
+ count: 1000,
128
+ end_time: end_time,
129
+ }.to_json
130
+ ].join("\r\n")
131
+
132
+ response_2 = [
133
+ "HTTP/1.1 200",
134
+ "Content-Type: application/json",
135
+ "",
136
+ {
137
+ tickets: [{"id" => 2}],
138
+ count: 2,
139
+ }.to_json
140
+ ].join("\r\n")
141
+
142
+ @httpclient.test_loopback_http_response << response_1
143
+ @httpclient.test_loopback_http_response << response_2
144
+
145
+ handler = proc { }
146
+ mock(handler).call(anything).twice
147
+ client.tickets(false, &handler)
148
+ end
149
+
150
+ test "raise DataError when invalid JSON response" do
151
+ @httpclient.test_loopback_http_response << [
152
+ "HTTP/1.1 200",
153
+ "Content-Type: application/json",
154
+ "",
155
+ "[[[" # invalid json
156
+ ].join("\r\n")
157
+
158
+ assert_raise(DataError) do
159
+ client.tickets(false)
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ sub_test_case "targets" do
166
+ def client
167
+ @client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 1, retry_initial_wait_sec: 0)
168
+ end
169
+
170
+ setup do
171
+ stub(Embulk).logger { Logger.new(File::NULL) }
172
+ @httpclient = client.httpclient
173
+ stub(client).httpclient { @httpclient }
174
+ end
175
+
176
+ sub_test_case "ticket_events" do
177
+ test "invoke incremental_export when partial=true" do
178
+ mock(client).incremental_export(anything, "ticket_events", anything, [])
179
+ client.ticket_events(true)
180
+ end
181
+
182
+ test "invoke incremental_export when partial=false" do
183
+ mock(client).incremental_export(anything, "ticket_events", anything, [])
184
+ client.ticket_events(false)
185
+ end
186
+ end
187
+
188
+ sub_test_case "ticket_fields" do
189
+ test "invoke export when partial=true" do
190
+ mock(client).export(anything, "ticket_fields", anything)
191
+ client.ticket_fields(true)
192
+ end
193
+
194
+ test "invoke export when partial=false" do
195
+ mock(client).export(anything, "ticket_fields", anything)
196
+ client.ticket_fields(false)
197
+ end
198
+ end
199
+
200
+ sub_test_case "ticket_forms" do
201
+ test "invoke export when partial=true" do
202
+ mock(client).export(anything, "ticket_forms", anything)
203
+ client.ticket_forms(true)
204
+ end
205
+
206
+ test "invoke export when partial=false" do
207
+ mock(client).export(anything, "ticket_forms", anything)
208
+ client.ticket_forms(false)
209
+ end
210
+ end
211
+ end
212
+
213
+
214
+ sub_test_case "auth" do
215
+ test "httpclient call validate_credentials" do
216
+ client = Client.new({})
217
+ mock(client).validate_credentials
218
+ client.httpclient
219
+ end
220
+
221
+ sub_test_case "auth_method: basic" do
222
+ test "don't raise on validate when username and password given" do
223
+ client = Client.new(login_url: login_url, auth_method: "basic", username: username, password: password)
224
+ assert_nothing_raised do
225
+ client.validate_credentials
226
+ end
227
+
228
+ any_instance_of(HTTPClient) do |klass|
229
+ mock(klass).set_auth(login_url, username, password)
230
+ end
231
+ client.httpclient
232
+ end
233
+
234
+ test "set_auth called with valid credential" do
235
+ client = Client.new(login_url: login_url, auth_method: "basic", username: username, password: password)
236
+
237
+ any_instance_of(HTTPClient) do |klass|
238
+ mock(klass).set_auth(login_url, username, password)
239
+ end
240
+ client.httpclient
241
+ end
242
+
243
+ data do
244
+ [
245
+ ["username", {username: "foo@example.com"}],
246
+ ["password", {password: "passWORD"}],
247
+ ["nothing both", {}],
248
+ ]
249
+ end
250
+ test "username only given" do |config|
251
+ client = Client.new(config.merge(auth_method: "basic"))
252
+ assert_raise(ConfigError) do
253
+ client.validate_credentials
254
+ end
255
+ end
256
+ end
257
+
258
+ sub_test_case "auth_method: token" do
259
+ test "don't raise on validate when username and token given" do
260
+ client = Client.new(login_url: login_url, auth_method: "token", username: username, token: token)
261
+ assert_nothing_raised do
262
+ client.validate_credentials
263
+ end
264
+ end
265
+
266
+ test "set_auth called with valid credential" do
267
+ client = Client.new(login_url: login_url, auth_method: "token", username: username, token: token)
268
+
269
+ any_instance_of(HTTPClient) do |klass|
270
+ mock(klass).set_auth(login_url, "#{username}/token", token)
271
+ end
272
+ client.httpclient
273
+ end
274
+
275
+ data do
276
+ [
277
+ ["username", {username: "foo@example.com"}],
278
+ ["token", {token: "TOKEN"}],
279
+ ["nothing both", {}],
280
+ ]
281
+ end
282
+ test "username only given" do |config|
283
+ client = Client.new(config.merge(auth_method: "token"))
284
+ assert_raise(ConfigError) do
285
+ client.validate_credentials
286
+ end
287
+ end
288
+ end
289
+
290
+ sub_test_case "auth_method: oauth" do
291
+ test "don't raise on validate when access_token given" do
292
+ client = Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token)
293
+ assert_nothing_raised do
294
+ client.validate_credentials
295
+ end
296
+ end
297
+
298
+ test "set default header with valid credential" do
299
+ client = Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token)
300
+
301
+ any_instance_of(HTTPClient) do |klass|
302
+ mock(klass).default_header = {
303
+ "Authorization" => "Bearer #{access_token}"
304
+ }
305
+ end
306
+ client.httpclient
307
+ end
308
+
309
+ test "access_token not given" do |config|
310
+ client = Client.new(auth_method: "oauth")
311
+ assert_raise(ConfigError) do
312
+ client.validate_credentials
313
+ end
314
+ end
315
+ end
316
+
317
+ sub_test_case "auth_method: unknown" do
318
+ test "raise on validate" do
319
+ client = Client.new(auth_method: "unknown")
320
+ assert_raise(ConfigError) do
321
+ client.validate_credentials
322
+ end
323
+ end
324
+ end
325
+ end
326
+
327
+ sub_test_case "retry" do
328
+ def client
329
+ @client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 2, retry_initial_wait_sec: 0)
330
+ end
331
+
332
+ def stub_response(status, headers = [])
333
+ @httpclient.test_loopback_http_response << [
334
+ "HTTP/1.1 #{status}",
335
+ "Content-Type: application/json",
336
+ headers.join("\r\n"),
337
+ "",
338
+ {
339
+ tickets: []
340
+ }.to_json
341
+ ].join("\r\n")
342
+ end
343
+
344
+ setup do
345
+ retryer = PerfectRetry.new do |conf|
346
+ conf.dont_rescues = [Exception] # Don't care any exceptions to retry
347
+ end
348
+
349
+ stub(Embulk).logger { Logger.new(File::NULL) }
350
+ @httpclient = client.httpclient
351
+ stub(client).httpclient { @httpclient }
352
+ stub(client).retryer { retryer }
353
+ PerfectRetry.disable!
354
+ end
355
+
356
+ teardown do
357
+ PerfectRetry.enable!
358
+ end
359
+
360
+ test "400" do
361
+ stub_response(400)
362
+ assert_raise(ConfigError) do
363
+ client.tickets(&proc{})
364
+ end
365
+ end
366
+
367
+ test "401" do
368
+ stub_response(401)
369
+ assert_raise(ConfigError) do
370
+ client.tickets(&proc{})
371
+ end
372
+ end
373
+
374
+ test "409" do
375
+ stub_response(409)
376
+ assert_raise(StandardError) do
377
+ client.tickets(&proc{})
378
+ end
379
+ end
380
+
381
+ test "429" do
382
+ after = "123"
383
+ stub_response(429, ["Retry-After: #{after}"])
384
+ mock(client).sleep after.to_i
385
+ assert_throw(:retry) do
386
+ client.tickets(&proc{})
387
+ end
388
+ end
389
+
390
+ test "500" do
391
+ stub_response(500)
392
+ assert_raise(StandardError) do
393
+ client.tickets(&proc{})
394
+ end
395
+ end
396
+
397
+ test "503" do
398
+ stub_response(503)
399
+ assert_raise(StandardError) do
400
+ client.tickets(&proc{})
401
+ end
402
+ end
403
+
404
+ test "503 with Retry-After" do
405
+ after = "123"
406
+ stub_response(503, ["Retry-After: #{after}"])
407
+ mock(client).sleep after.to_i
408
+ assert_throw(:retry) do
409
+ client.tickets(&proc{})
410
+ end
411
+ end
412
+
413
+ test "Unhandled response code (555)" do
414
+ stub_response(555)
415
+ assert_raise(RuntimeError.new("Server returns unknown status code (555)")) do
416
+ client.tickets(&proc{})
417
+ end
418
+ end
419
+ end
420
+
421
+ sub_test_case ".validate_target" do
422
+ data do
423
+ [
424
+ ["tickets", ["tickets", nil]],
425
+ ["ticket_events", ["ticket_events", nil]],
426
+ ["users", ["users", nil]],
427
+ ["organizations", ["organizations", nil]],
428
+ ["unknown", ["unknown", Embulk::ConfigError]],
429
+ ]
430
+ end
431
+ test "validate with target" do |data|
432
+ target, error = data
433
+ client = Client.new({target: target})
434
+
435
+ if error
436
+ assert_raise(error) do
437
+ client.validate_target
438
+ end
439
+ else
440
+ assert_nothing_raised do
441
+ client.validate_target
442
+ end
443
+ end
444
+ end
445
+ end
446
+
447
+ def login_url
448
+ "http://example.com"
449
+ end
450
+
451
+ def username
452
+ "foo@example.com"
453
+ end
454
+
455
+ def password
456
+ "passWORD"
457
+ end
458
+
459
+ def token
460
+ "TOKEN"
461
+ end
462
+
463
+ def access_token
464
+ "ACCESS_TOKEN"
465
+ end
466
+ end
467
+ end
468
+ end
469
+ end