embulk-input-zendesk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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