itrp-client 1.0.14 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 147dc4446fd8b16a6719124e8ab21a41d200ae5c
4
- data.tar.gz: fcad65d016b1d9f06d4868cddcf8a9557a7844fc
3
+ metadata.gz: 89081124a0ce058991107658acc89152c8d4118e
4
+ data.tar.gz: b92af38225ebdbd0592f508b4e5ddd7fb6c6983c
5
5
  SHA512:
6
- metadata.gz: 25d6c3c1ceb110176e1a86ed019db1c79766db489785adec61dfa67cc8742cfff5f460d33d78830ff2b632b3a6b6cc098f3ccc72d0321b652e225bdc02406aff
7
- data.tar.gz: 5faa62a7886b27adc35b3edfe86078d372521fbb6bef80c66e749411a57ce49394a2630b54f20fdb456c02ea8db43be14ee0ffa76b3e149ee019b60df4d6db86
6
+ metadata.gz: 232eeb5ef89f2e2f6c83266eb95a777ecccfa891e9b7fed8ea88c51c332183c7ffec3aaaed8eb259c66a540a0d86d0152fb65612aab779f463cd8ad7b24f6249
7
+ data.tar.gz: acc1b94b7a6ea21c695a0f44ef3d332a601f0ac3c77af43499c79cb386424aa0958498a5d2cb62b4184f83eb96b04356b99129b3f693f81663767db02e257cf4
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- itrp-client (1.0.13)
4
+ itrp-client (1.1.0)
5
5
  activesupport
6
6
  gem_config
7
7
  mime-types
@@ -9,26 +9,28 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- activesupport (4.2.5.1)
12
+ activesupport (4.2.7.1)
13
13
  i18n (~> 0.7)
14
14
  json (~> 1.7, >= 1.7.7)
15
15
  minitest (~> 5.1)
16
16
  thread_safe (~> 0.3, >= 0.3.4)
17
17
  tzinfo (~> 1.1)
18
- addressable (2.4.0)
18
+ addressable (2.5.0)
19
+ public_suffix (~> 2.0, >= 2.0.2)
19
20
  crack (0.4.3)
20
21
  safe_yaml (~> 1.0.0)
21
22
  diff-lcs (1.2.5)
22
23
  docile (1.1.5)
23
24
  gem_config (0.3.1)
24
- hashdiff (0.2.3)
25
+ hashdiff (0.3.1)
25
26
  i18n (0.7.0)
26
27
  json (1.8.3)
27
- mime-types (3.0)
28
+ mime-types (3.1)
28
29
  mime-types-data (~> 3.2015)
29
- mime-types-data (3.2015.1120)
30
- minitest (5.8.4)
31
- rake (10.5.0)
30
+ mime-types-data (3.2016.0521)
31
+ minitest (5.9.1)
32
+ public_suffix (2.0.4)
33
+ rake (11.3.0)
32
34
  rspec (3.3.0)
33
35
  rspec-core (~> 3.3.0)
34
36
  rspec-expectations (~> 3.3.0)
@@ -43,15 +45,15 @@ GEM
43
45
  rspec-support (~> 3.3.0)
44
46
  rspec-support (3.3.0)
45
47
  safe_yaml (1.0.4)
46
- simplecov (0.11.1)
48
+ simplecov (0.12.0)
47
49
  docile (~> 1.1.0)
48
- json (~> 1.8)
50
+ json (>= 1.8, < 3)
49
51
  simplecov-html (~> 0.10.0)
50
52
  simplecov-html (0.10.0)
51
53
  thread_safe (0.3.5)
52
54
  tzinfo (1.2.2)
53
55
  thread_safe (~> 0.1)
54
- webmock (1.22.6)
56
+ webmock (2.1.0)
55
57
  addressable (>= 2.3.6)
56
58
  crack (>= 0.3.2)
57
59
  hashdiff
@@ -65,7 +67,7 @@ DEPENDENCIES
65
67
  rake
66
68
  rspec (~> 3.3.0)
67
69
  simplecov
68
- webmock
70
+ webmock (~> 2)
69
71
 
70
72
  BUNDLED WITH
71
- 1.10.6
73
+ 1.11.2
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 ITRP Inc.
1
+ Copyright (c) 2016 ITRP Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
7
7
  spec.name = "itrp-client"
8
8
  spec.version = Itrp::Client::VERSION
9
9
  spec.platform = Gem::Platform::RUBY
10
- spec.required_ruby_version = '>= 1.9.3'
10
+ spec.required_ruby_version = '>= 2.0.0'
11
11
  spec.authors = ["ITRP"]
12
12
  spec.email = %q{developers@itrp.com}
13
13
  spec.description = %q{Client for accessing the ITRP REST API}
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_development_dependency "bundler", "~> 1.3"
35
35
  spec.add_development_dependency "rake"
36
36
  spec.add_development_dependency 'rspec', "~> 3.3.0"
37
- spec.add_development_dependency 'webmock'
37
+ spec.add_development_dependency 'webmock', "~> 2"
38
38
  spec.add_development_dependency 'simplecov'
39
39
 
40
40
  end
@@ -225,7 +225,7 @@ module Itrp
225
225
  path
226
226
  end
227
227
 
228
- # Expand one parameter, e.g. (:"created_at=>", DateTime.now) to "created_at=%3E22011-12-16T12:24:41+01:00"
228
+ # Expand one parameter, e.g. (:"created_at=>", DateTime.now) to "created_at=%3E22011-12-16T12:24:41%2B01:00"
229
229
  def expand_param(key, value)
230
230
  param = uri_escape(key.to_s).gsub('%3D', '=') # handle :"updated_at=>" or :"person_id!=" parameters
231
231
  param << '=' unless key['=']
@@ -237,10 +237,10 @@ module Itrp
237
237
  def typecast(value, escape = true)
238
238
  case value.class.name.to_sym
239
239
  when :NilClass then ''
240
- when :String then escape ? URI.escape(value) : value
240
+ when :String then escape ? uri_escape(value) : value
241
241
  when :TrueClass then 'true'
242
242
  when :FalseClass then 'false'
243
- when :DateTime then value.new_offset(0).iso8601
243
+ when :DateTime then datetime = value.new_offset(0).iso8601; escape ? uri_escape(datetime) : datetime
244
244
  when :Date then value.strftime("%Y-%m-%d")
245
245
  when :Time then value.strftime("%H:%M")
246
246
  # do not convert arrays in put/post requests as squashing arrays is only used in filtering
@@ -278,41 +278,45 @@ module Itrp
278
278
  response
279
279
  end
280
280
 
281
+ # parse the given URI to [domain, port, ssl, path]
282
+ def ssl_domain_port_path(uri)
283
+ uri = URI.parse(uri)
284
+ ssl = uri.scheme == 'https'
285
+ [ssl, uri.host, uri.port, uri.path]
286
+ end
287
+
288
+ end
289
+
290
+ module SendWithRateLimitBlock
281
291
  # Wraps the _send method with retries when the server does not responsd, see +initialize+ option +:rate_limit_block+
282
- def _send_with_rate_limit_block(request, domain = @domain, port = @port, ssl = @ssl)
283
- return _send_without_rate_limit_block(request, domain, port, ssl) unless option(:block_at_rate_limit)
292
+ def _send(request, domain = @domain, port = @port, ssl = @ssl)
293
+ return super(request, domain, port, ssl) unless option(:block_at_rate_limit)
284
294
  now = Time.now
285
295
  begin
286
- _response = _send_without_rate_limit_block(request, domain, port, ssl)
296
+ _response = super(request, domain, port, ssl)
287
297
  @logger.warn { "Request throttled, trying again in 5 minutes: #{_response.message}" } and sleep(300) if _response.throttled?
288
298
  end while _response.throttled? && (Time.now - now) < 3660 # max 1 hour and 1 minute
289
299
  _response
290
300
  end
291
- alias_method_chain :_send, :rate_limit_block
301
+ end
302
+ Client.send(:prepend, SendWithRateLimitBlock)
292
303
 
304
+ module SendWithRetries
293
305
  # Wraps the _send method with retries when the server does not responsd, see +initialize+ option +:retries+
294
- def _send_with_retries(request, domain = @domain, port = @port, ssl = @ssl)
306
+ def _send(request, domain = @domain, port = @port, ssl = @ssl)
295
307
  retries = 0
296
308
  sleep_time = 2
297
309
  total_retry_time = 0
298
310
  begin
299
- _response = _send_without_retries(request, domain, port, ssl)
311
+ _response = super(request, domain, port, ssl)
300
312
  @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{_response.message}" } and sleep(sleep_time) if _response.empty? && option(:max_retry_time) > 0
301
313
  total_retry_time += sleep_time
302
314
  sleep_time *= 2
303
315
  end while _response.empty? && total_retry_time < option(:max_retry_time)
304
316
  _response
305
317
  end
306
- alias_method_chain :_send, :retries
307
-
308
- # parse the given URI to [domain, port, ssl, path]
309
- def ssl_domain_port_path(uri)
310
- uri = URI.parse(uri)
311
- ssl = uri.scheme == 'https'
312
- [ssl, uri.host, uri.port, uri.path]
313
- end
314
-
315
318
  end
319
+ Client.send(:prepend, SendWithRetries)
316
320
  end
317
321
 
318
322
  # HTTPS with certificate bundle
@@ -1,5 +1,5 @@
1
1
  module Itrp
2
2
  class Client
3
- VERSION = '1.0.14'
3
+ VERSION = '1.1.0'
4
4
  end
5
5
  end
@@ -0,0 +1,178 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itrp::Attachments do
4
+
5
+ before(:each) do
6
+ @client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
7
+ @attachments = Itrp::Attachments.new(@client)
8
+ end
9
+
10
+ context 'upload_attachments!' do
11
+ it 'should not do anything when no :attachments are present' do
12
+ expect(@attachments.upload_attachments!('/requests', {status: :in_progress})).to be_nil
13
+ end
14
+
15
+ it 'should not do anything when :attachments is nil' do
16
+ expect(@attachments.upload_attachments!('/requests', {attachments: nil})).to be_nil
17
+ end
18
+
19
+ it 'should not do anything when :attachments is empty' do
20
+ expect(@attachments.upload_attachments!('/requests', {attachments: []})).to be_nil
21
+ expect(@attachments.upload_attachments!('/requests', {attachments: [nil]})).to be_nil
22
+ end
23
+
24
+ it 'should show a error if no attachment may be uploaded' do
25
+ stub_request(:get, 'https://api.itrp.com/v1/sites/1?attachment_upload_token=true').with(basic_auth: ['secret', 'x']).to_return(body: {name: 'site 1'}.to_json)
26
+ expect_log('Attachments not allowed for /sites/1', :error)
27
+ expect(@attachments.upload_attachments!('/sites/1', {attachments: ['file1.png']})).to be_nil
28
+ end
29
+
30
+ it 'should raise an exception if no attachment may be uploaded' do
31
+ stub_request(:get, 'https://api.itrp.com/v1/sites/1?attachment_upload_token=true').with(basic_auth: ['secret', 'x']).to_return(body: {name: 'site 1'}.to_json)
32
+ message = 'Attachments not allowed for /sites/1'
33
+ expect{ @attachments.upload_attachments!('/sites/1', {attachments: ['file1.png'], attachments_exception: true}) }.to raise_error(::Itrp::UploadFailed, message)
34
+ end
35
+
36
+ it 'should add /new to the path for new records' do
37
+ stub_request(:get, 'https://api.itrp.com/v1/sites/new?attachment_upload_token=true').with(basic_auth: ['secret', 'x']).to_return(body: {missing: 'storage'}.to_json)
38
+ expect_log('Attachments not allowed for /sites', :error)
39
+ expect(@attachments.upload_attachments!('/sites', {attachments: ['file1.png']})).to be_nil
40
+ end
41
+
42
+ [ [:requests, :note],
43
+ [:problems, :note],
44
+ [:contracts, :remarks],
45
+ [:cis, :remarks],
46
+ [:flsas, :remarks],
47
+ [:slas, :remarks],
48
+ [:service_instances, :remarks],
49
+ [:service_offerings, :summary],
50
+ [:any_other_model, :note]].each do |model, attribute|
51
+
52
+ it "should replace :attachments with :#{attribute}_attachments after upload at /#{model}" do
53
+ stub_request(:get, "https://api.itrp.com/v1/#{model}/new?attachment_upload_token=true").with(basic_auth: ['secret', 'x']).to_return(body: {storage_upload: 'conf'}.to_json)
54
+ expect(@attachments).to receive(:upload_attachment).with('conf', 'file1.png', false).ordered{ 'uploaded file1.png' }
55
+ expect(@attachments).to receive(:upload_attachment).with('conf', 'file2.zip', false).ordered{ 'uploaded file2.zip' }
56
+ data = {leave: 'me alone', attachments: %w(file1.png file2.zip)}
57
+ @attachments.upload_attachments!("/#{model}", data)
58
+ expect(data[:attachments]).to be_nil
59
+ expect(data[:leave]).to eq('me alone')
60
+ expect(data[:"#{attribute}_attachments"]).to eq(['uploaded file1.png', 'uploaded file2.zip'].to_json)
61
+ end
62
+ end
63
+
64
+ it 'should set raise_exception flag to true when :attachments_exception is set' do
65
+ stub_request(:get, 'https://api.itrp.com/v1/requests/new?attachment_upload_token=true').with(basic_auth: ['secret', 'x']).to_return(body: {storage_upload: 'conf'}.to_json)
66
+ expect(@attachments).to receive(:upload_attachment).with('conf', 'file1.png', true).ordered{ 'uploaded file1.png' }
67
+ data = {leave: 'me alone', attachments: 'file1.png', attachments_exception: true}
68
+ @attachments.upload_attachments!('/requests', data)
69
+ expect(data[:attachments]).to be_nil
70
+ expect(data[:attachments_exception]).to be_nil
71
+ expect(data[:leave]).to eq('me alone')
72
+ expect(data[:note_attachments]).to eq(['uploaded file1.png'].to_json)
73
+ end
74
+ end
75
+
76
+ context 'upload_attachment' do
77
+
78
+ it 'should log an exception when the file could not be found' do
79
+ expect_log('Attachment upload failed: file does not exist: unknown_file', :error)
80
+ expect(@attachments.send(:upload_attachment, nil, 'unknown_file', false)).to be_nil
81
+ end
82
+
83
+ it 'should raise an exception when the file could not be found' do
84
+ message = 'Attachment upload failed: file does not exist: unknown_file'
85
+ expect{ @attachments.send(:upload_attachment, nil, 'unknown_file', true) }.to raise_error(::Itrp::UploadFailed, message)
86
+ end
87
+
88
+ context 'aws' do
89
+ before(:each) do
90
+ @aws_conf = {
91
+ provider: 'aws',
92
+ upload_uri: 'https://itrp.s3.amazonaws.com/',
93
+ access_key: "AKIA6RYQ",
94
+ success_url: "https://mycompany.itrp.com/s3_success?sig=99e82e8a046",
95
+ policy: "eydlgIH0=",
96
+ signature: 'nbhdec4k=',
97
+ upload_path: 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/'
98
+ }
99
+ @key_template = 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}'
100
+ @key = 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/upload.txt'
101
+
102
+ @multi_part_body = "--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"Content-Type\"\r\n\r\napplication/octet-stream\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"x-amz-server-side-encryption\"\r\n\r\nAES256\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\nattachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"AWSAccessKeyId\"\r\n\r\nAKIA6RYQ\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"acl\"\r\n\r\nprivate\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"signature\"\r\n\r\nnbhdec4k=\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"success_action_status\"\r\n\r\n201\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"policy\"\r\n\r\neydlgIH0=\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"file\"; filename=\"#{@fixture_dir}/upload.txt\"\r\nContent-Type: text/plain\r\n\r\ncontent\r\n--0123456789ABLEWASIEREISAWELBA9876543210--"
103
+ @multi_part_headers = {'Accept'=>'*/*', 'Content-Type'=>'multipart/form-data; boundary=0123456789ABLEWASIEREISAWELBA9876543210', 'User-Agent'=>'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6'}
104
+ end
105
+
106
+ it 'should open a file from disk' do
107
+ expect(@attachments).to receive(:aws_upload).with(@aws_conf, @key_template, @key, kind_of(File))
108
+ expect(@attachments.send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", false)).to eq({key: @key, filesize: 7})
109
+ end
110
+
111
+ it 'should sent the upload to AWS' do
112
+ stub_request(:post, 'https://itrp.s3.amazonaws.com/').with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: 'OK', status: 303, headers: {'Location' => 'https://mycompany.itrp.com/s3_success?sig=99e82e8a046'})
113
+ stub_request(:get, "https://api.itrp.com/v1/s3_success?sig=99e82e8a046&key=#{@key}").with(basic_auth: ['secret', 'x']).to_return(body: {}.to_json)
114
+ expect(@attachments.send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", false)).to eq({key: @key, filesize: 7})
115
+ end
116
+
117
+ it 'should report an error when AWS upload fails' do
118
+ stub_request(:post, 'https://itrp.s3.amazonaws.com/').with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: %(<?xml version="1.0" encoding="UTF-8"?>\n<Error><Code>AccessDenied</Code><Message>Invalid according to Policy</Message><RequestId>1FECC4B719E426B1</RequestId><HostId>15+14lXt+HlF</HostId></Error>), status: 303, headers: {'Location' => 'https://mycompany.itrp.com/s3_success?sig=99e82e8a046'})
119
+ expect_log("Attachment upload failed: AWS upload to https://itrp.s3.amazonaws.com/ for #{@key} failed: Invalid according to Policy", :error)
120
+ expect(@attachments.send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", false)).to be_nil
121
+ end
122
+
123
+ it 'should report an error when ITRP confirmation fails' do
124
+ stub_request(:post, 'https://itrp.s3.amazonaws.com/').with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: 'OK', status: 303, headers: {'Location' => 'https://mycompany.itrp.com/s3_success?sig=99e82e8a046'})
125
+ stub_request(:get, "https://api.itrp.com/v1/s3_success?sig=99e82e8a046&key=#{@key}").with(basic_auth: ['secret', 'x']).to_return(body: {message: 'oops!'}.to_json)
126
+ expect_log('Request failed: oops!', :error)
127
+ expect_log("Attachment upload failed: ITRP confirmation s3_success?sig=99e82e8a046 for #{@key} failed: oops!", :error)
128
+ expect(@attachments.send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", false)).to be_nil
129
+ end
130
+
131
+ it 'should raise an exception when AWS upload fails' do
132
+ stub_request(:post, 'https://itrp.s3.amazonaws.com/').with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: %(<?xml version="1.0" encoding="UTF-8"?>\n<Error><Code>AccessDenied</Code><Message>Invalid according to Policy</Message><RequestId>1FECC4B719E426B1</RequestId><HostId>15+14lXt+HlF</HostId></Error>), status: 303, headers: {'Location' => 'https://mycompany.itrp.com/s3_success?sig=99e82e8a046'})
133
+ message = "Attachment upload failed: AWS upload to https://itrp.s3.amazonaws.com/ for #{@key} failed: Invalid according to Policy"
134
+ expect{ @attachments.send(:upload_attachment, @aws_conf, "#{@fixture_dir}/upload.txt", true) }.to raise_error(::Itrp::UploadFailed, message)
135
+ end
136
+ end
137
+
138
+ context 'itrp' do
139
+ before(:each) do
140
+ @itrp_conf = {
141
+ provider: 'local',
142
+ upload_uri: 'https://api.itrp.com/attachments',
143
+ upload_path: 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/'
144
+ }
145
+ @key_template = 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}'
146
+ @key = 'attachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/upload.txt'
147
+
148
+ @multi_part_body = "--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"Content-Type\"\r\n\r\napplication/octet-stream\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"file\"; filename=\"#{@spec_dir}/support/fixtures/upload.txt\"\r\nContent-Type: text/plain\r\n\r\ncontent\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\nattachments/5/reqs/000/070/451/zxxb4ot60xfd6sjg/${filename}\r\n--0123456789ABLEWASIEREISAWELBA9876543210--"
149
+ @multi_part_headers = {'Accept'=>'*/*', 'Content-Type'=>'multipart/form-data; boundary=0123456789ABLEWASIEREISAWELBA9876543210', 'User-Agent'=>'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6'}
150
+ end
151
+
152
+ it 'should open a file from disk' do
153
+ expect(@attachments).to receive(:itrp_upload).with(@itrp_conf, @key_template, @key, kind_of(File))
154
+ expect(@attachments.send(:upload_attachment, @itrp_conf, "#{@fixture_dir}/upload.txt", false)).to eq({key: @key, filesize: 7})
155
+ end
156
+
157
+ it 'should sent the upload to ITRP' do
158
+ stub_request(:post, 'https://api.itrp.com/v1/attachments').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {}.to_json)
159
+ expect(@attachments.send(:upload_attachment, @itrp_conf, "#{@fixture_dir}/upload.txt", false)).to eq({key: @key, filesize: 7})
160
+ end
161
+
162
+ it 'should report an error when ITRP upload fails' do
163
+ stub_request(:post, 'https://api.itrp.com/v1/attachments').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {message: 'oops!'}.to_json)
164
+ expect_log('Request failed: oops!', :error)
165
+ expect_log("Attachment upload failed: ITRP upload to https://api.itrp.com/attachments for #{@key} failed: oops!", :error)
166
+ expect(@attachments.send(:upload_attachment, @itrp_conf, "#{@fixture_dir}/upload.txt", false)).to be_nil
167
+ end
168
+
169
+ it 'should raise an exception when ITRP upload fails' do
170
+ stub_request(:post, 'https://api.itrp.com/v1/attachments').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {message: 'oops!'}.to_json)
171
+ expect_log('Request failed: oops!', :error)
172
+ message = "Attachment upload failed: ITRP upload to https://api.itrp.com/attachments for #{@key} failed: oops!"
173
+ expect{ @attachments.send(:upload_attachment, @itrp_conf, "#{@fixture_dir}/upload.txt", true) }.to raise_error(::Itrp::UploadFailed, message)
174
+ end
175
+ end
176
+
177
+ end
178
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ca-bundle.crt' do
4
+
5
+ it 'should be able to connect to the ITRP API' do
6
+ WebMock.allow_net_connect!
7
+ client = Itrp::Client.new(api_token: 'invalid', max_retry_time: -1)
8
+ result = {}
9
+
10
+ # no exception concerning the certificate
11
+ expect { result[:response] = client.get('me') }.not_to raise_error
12
+ response = result[:response]
13
+ expect(response.valid?).to be_falsey
14
+
15
+ # expecting 401 error
16
+ expect(response.message).to eq('401: Access credentials required')
17
+ end
18
+
19
+ it 'should be able to connect to S3' do
20
+ WebMock.allow_net_connect!
21
+ http = Net::HTTP.new('itrp-eu.s3-eu-west-1.amazonaws.com', 443)
22
+ http.read_timeout = 1
23
+ http.use_ssl = true
24
+
25
+ # no SSL error please
26
+ expect{ http.start{ |_http| _http.request(Net::HTTP::Get.new('/exports/20141107/')) } }.to never_raise(OpenSSL::SSL::SSLError)
27
+ end
28
+ end
@@ -0,0 +1,527 @@
1
+ require 'spec_helper'
2
+
3
+ describe Itrp::Client do
4
+
5
+ context 'Itrp.config' do
6
+ before(:each) do
7
+ Itrp.configure do |config|
8
+ config.max_retry_time = 120 # override default value (5400)
9
+ config.api_token = 'secret' # set value
10
+ end
11
+ end
12
+
13
+ it 'should define the MAX_PAGE_SIZE' do
14
+ expect(Itrp::Client::MAX_PAGE_SIZE).to eq(100)
15
+ end
16
+
17
+ it 'should use the Itrp configuration' do
18
+ client = Itrp::Client.new
19
+ expect(client.option(:host)).to eq('https://api.itrp.com') # default value
20
+ expect(client.option(:api_token)).to eq('secret') # value set using Itrp.config
21
+ expect(client.option(:max_retry_time)).to eq(120) # value overridden in Itrp.config
22
+ end
23
+
24
+ it 'should override the Itrp configuration' do
25
+ client = Itrp::Client.new(host: 'https://demo.itrp.com', api_token: 'unknown', block_at_rate_limit: true)
26
+ expect(client.option(:read_timeout)).to eq(25) # default value
27
+ expect(client.option(:host)).to eq('https://demo.itrp.com') # default value overridden in Client.new
28
+ expect(client.option(:api_token)).to eq('unknown') # value set using Itrp.config and overridden in Client.new
29
+ expect(client.option(:max_retry_time)).to eq(120) # value overridden in Itrp.config
30
+ expect(client.option(:block_at_rate_limit)).to eq(true) # value overridden in Client.new
31
+ end
32
+
33
+ [:host, :api_version, :api_token].each do |required_option|
34
+ it "should require option #{required_option}" do
35
+ expect { Itrp::Client.new(required_option => '') }.to raise_error("Missing required configuration option #{required_option}")
36
+ end
37
+ end
38
+
39
+ [ ['https://api.itrp.com', true, 'api.itrp.com', 443],
40
+ ['https://api.example.com:777', true, 'api.example.com', 777],
41
+ ['http://itrp.example.com', false, 'itrp.example.com', 80],
42
+ ['http://itrp.example.com:777', false, 'itrp.example.com', 777]
43
+ ].each do |host, ssl, domain, port|
44
+ it 'should parse ssl, host and port' do
45
+ client = Itrp::Client.new(host: host)
46
+ expect(client.instance_variable_get(:@ssl)).to eq(ssl)
47
+ expect(client.instance_variable_get(:@domain)).to eq(domain)
48
+ expect(client.instance_variable_get(:@port)).to eq(port)
49
+ end
50
+ end
51
+ end
52
+
53
+ it 'should set the ca-bundle.crt file' do
54
+ http = Net::HTTP.new('https://api.itrp.com')
55
+ http.use_ssl = true
56
+
57
+ on_disk = `ls #{http.ca_file}`
58
+ expect(on_disk).not_to match(/cannot access/)
59
+ expect(on_disk).to match(/\/ca-bundle.crt$/)
60
+ end
61
+
62
+ describe 'headers' do
63
+ before(:each) do
64
+ @client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
65
+ end
66
+
67
+ it 'should set the content type header' do
68
+ stub = stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).with(headers: {'Content-Type' => 'application/json'}).to_return(body: {name: 'my name'}.to_json)
69
+ @client.get('me')
70
+ expect(stub).to have_been_requested
71
+ end
72
+
73
+ it 'should add the X-ITRP-Account header' do
74
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1, account: 'test')
75
+ stub = stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).with(headers: {'X-ITRP-Account' => 'test'}).to_return(body: {name: 'my name'}.to_json)
76
+ client.get('me')
77
+ expect(stub).to have_been_requested
78
+ end
79
+
80
+ it 'should add the X-ITRP-Source header' do
81
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1, source: 'myapp')
82
+ stub = stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).with(headers: {'X-ITRP-Source' => 'myapp'}).to_return(body: {name: 'my name'}.to_json)
83
+ client.get('me')
84
+ expect(stub).to have_been_requested
85
+ end
86
+
87
+ it 'should be able to override headers' do
88
+ stub = stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).with(headers: {'Content-Type' => 'application/x-www-form-urlencoded'}).to_return(body: {name: 'my name'}.to_json)
89
+ @client.get('me', {}, {'Content-Type' => 'application/x-www-form-urlencoded'})
90
+ expect(stub).to have_been_requested
91
+ end
92
+
93
+ it 'should set the other headers' do
94
+ stub = stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).with(headers: {'X-ITRP-Other' => 'value'}).to_return(body: {name: 'my name'}.to_json)
95
+ @client.get('me', {}, {'X-ITRP-Other' => 'value'})
96
+ expect(stub).to have_been_requested
97
+ end
98
+
99
+ it 'should accept headers in the each call' do
100
+ stub = stub_request(:get, 'https://api.itrp.com/v1/requests?fields=subject&page=1&per_page=100').with(basic_auth: ['secret', 'x']).with(headers: {'X-ITRP-Secret' => 'special'}).to_return(body: [{id: 1, subject: 'Subject 1'}, {id: 2, subject: 'Subject 2'}, {id: 3, subject: 'Subject 3'}].to_json)
101
+ @client.each('requests', {fields: 'subject'}, {'X-ITRP-Secret' => 'special'}) do |request|
102
+ expect(request[:subject]).to eq("Subject #{request[:id]}")
103
+ end
104
+ expect(stub).to have_been_requested
105
+ end
106
+ end
107
+
108
+ context 'each' do
109
+ before(:each) do
110
+ @client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
111
+ end
112
+
113
+ it 'should yield each result' do
114
+ stub_request(:get, 'https://api.itrp.com/v1/requests?fields=subject&page=1&per_page=100').with(basic_auth: ['secret', 'x']).to_return(body: [{id: 1, subject: 'Subject 1'}, {id: 2, subject: 'Subject 2'}, {id: 3, subject: 'Subject 3'}].to_json)
115
+ nr_of_requests = @client.each('requests', {fields: 'subject'}) do |request|
116
+ expect(request[:subject]).to eq("Subject #{request[:id]}")
117
+ end
118
+ expect(nr_of_requests).to eq(3)
119
+ end
120
+
121
+ it 'should retrieve multiple pages' do
122
+ stub_page1 = stub_request(:get, 'https://api.itrp.com/v1/requests?page=1&per_page=2').with(basic_auth: ['secret', 'x']).to_return(body: [{id: 1, subject: 'Subject 1'}, {id: 2, subject: 'Subject 2'}].to_json, headers: {'Link' => '<https://api.itrp.com/v1/requests?page=1&per_page=2>; rel="first",<https://api.itrp.com/v1/requests?page=2&per_page=2>; rel="next",<https://api.itrp.com/v1/requests?page=2&per_page=2>; rel="last"'})
123
+ stub_page2 = stub_request(:get, 'https://api.itrp.com/v1/requests?page=2&per_page=2').with(basic_auth: ['secret', 'x']).to_return(body: [{id: 3, subject: 'Subject 3'}].to_json, headers: {'Link' => '<https://api.itrp.com/v1/requests?page=1&per_page=2>; rel="first",<https://api.itrp.com/v1/requests?page=1&per_page=2>; rel="prev",<https://api.itrp.com/v1/requests?page=2&per_page=2>; rel="last"'})
124
+ nr_of_requests = @client.each('requests', {per_page: 2}) do |request|
125
+ expect(request[:subject]).to eq("Subject #{request[:id]}")
126
+ end
127
+ expect(nr_of_requests).to eq(3)
128
+ expect(stub_page2).to have_been_requested
129
+ end
130
+ end
131
+
132
+ context 'get' do
133
+ before(:each) do
134
+ @client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
135
+ end
136
+
137
+ it 'should return a response' do
138
+ stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).to_return(body: {name: 'my name'}.to_json)
139
+ response = @client.get('me')
140
+ expect(response[:name]).to eq('my name')
141
+ end
142
+
143
+ describe 'parameters' do
144
+
145
+ [[nil, ''],
146
+ [ 'normal', 'normal'],
147
+ [ 'hello;<', 'hello%3B%3C'],
148
+ [ true, 'true'],
149
+ [ false, 'false'],
150
+ [ DateTime.now, DateTime.now.new_offset(0).iso8601.gsub('+', '%2B')],
151
+ [ Date.new, Date.new.strftime('%Y-%m-%d')],
152
+ [ Time.now, Time.now.strftime('%H:%M')],
153
+ [ ['first', 'second;<', true], 'first,second%3B%3C,true']
154
+ ].each do |param_value, url_value|
155
+ it "should cast #{param_value.class.name}: '#{param_value}' to '#{url_value}'" do
156
+ stub = stub_request(:get, "https://api.itrp.com/v1/me?value=#{url_value}").with(basic_auth: ['secret', 'x']).to_return(body: {name: 'my name'}.to_json)
157
+ @client.get('me', {value: param_value})
158
+ expect(stub).to have_been_requested
159
+ end
160
+ end
161
+
162
+ it 'should not cast arrays in post and put calls' do
163
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
164
+ stub = stub_request(:post, 'https://api.itrp.com/v1/people').with(basic_auth: ['secret', 'x']).with(body: {user_ids: [1, 2, 3]}, headers: {'X-ITRP-Custom' => 'custom'}).to_return(body: {id: 101}.to_json)
165
+ client.post('people', {user_ids: [1, 2, 3]}, {'X-ITRP-Custom' => 'custom'})
166
+ expect(stub).to have_been_requested
167
+ end
168
+
169
+ it 'should not cast hashes in post and put calls' do
170
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
171
+ stub = stub_request(:patch, 'https://api.itrp.com/v1/people/55').with(basic_auth: ['secret', 'x']).with(body: '{"contacts_attributes":{"0":{"protocol":"email","label":"work","uri":"work@example.com"}}}', headers: {'X-ITRP-Custom' => 'custom'}).to_return(body: {id: 101}.to_json)
172
+ client.put('people/55', {contacts_attributes: {0 => {protocol: :email, label: :work, uri: 'work@example.com'}}}, {'X-ITRP-Custom' => 'custom'})
173
+ expect(stub).to have_been_requested
174
+ end
175
+
176
+ it 'should not double escape symbols' do
177
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
178
+ stub = stub_request(:patch, 'https://api.itrp.com/v1/people/55').with(basic_auth: ['secret', 'x']).with(body: '{"status":"waiting_for"}').to_return(body: {id: 101}.to_json)
179
+ client.put('people/55', {status: :waiting_for})
180
+ expect(stub).to have_been_requested
181
+ end
182
+
183
+ it 'should handle fancy filter operations' do
184
+ now = DateTime.now
185
+ stub = stub_request(:get, "https://api.itrp.com/v1/people?created_at=>#{now.new_offset(0).iso8601.gsub('+', '%2B')}&id!=15").with(basic_auth: ['secret', 'x']).to_return(body: {name: 'my name'}.to_json)
186
+ @client.get('people', {'created_at=>' => now, 'id!=' => 15})
187
+ expect(stub).to have_been_requested
188
+ end
189
+
190
+ it 'should append parameters' do
191
+ stub = stub_request(:get, 'https://api.itrp.com/v1/people?id!=15&primary_email=me@example.com').with(basic_auth: ['secret', 'x']).to_return(body: {name: 'my name'}.to_json)
192
+ @client.get('people?id!=15', {primary_email: 'me@example.com'})
193
+ expect(stub).to have_been_requested
194
+ end
195
+ end
196
+ end
197
+
198
+ context 'patch' do
199
+ [:put, :patch].each do |method|
200
+ it 'should send patch requests with parameters and headers for #{method} calls' do
201
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
202
+ stub = stub_request(:patch, 'https://api.itrp.com/v1/people/1').with(basic_auth: ['secret', 'x']).with(body: {name: 'New Name'}, headers: {'X-ITRP-Custom' => 'custom'}).to_return(body: {id: 1}.to_json)
203
+ client.send(method, 'people/1', {name: 'New Name'}, {'X-ITRP-Custom' => 'custom'})
204
+ expect(stub).to have_been_requested
205
+ end
206
+ end
207
+ end
208
+
209
+ context 'post' do
210
+ it 'should send post requests with parameters and headers' do
211
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
212
+ stub = stub_request(:post, 'https://api.itrp.com/v1/people').with(basic_auth: ['secret', 'x']).with(body: {name: 'New Name'}, headers: {'X-ITRP-Custom' => 'custom'}).to_return(body: {id: 101}.to_json)
213
+ client.post('people', {name: 'New Name'}, {'X-ITRP-Custom' => 'custom'})
214
+ expect(stub).to have_been_requested
215
+ end
216
+ end
217
+
218
+ context 'delete' do
219
+ it 'should send delete requests with parameters and headers' do
220
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
221
+ stub = stub_request(:delete, 'https://api.itrp.com/v1/people?id=value').with(basic_auth: ['secret', 'x']).with(headers: {'X-ITRP-Custom' => 'custom'}).to_return(body: {id: 101}.to_json)
222
+ client.delete('people', {id: 'value'}, {'X-ITRP-Custom' => 'custom'})
223
+ expect(stub).to have_been_requested
224
+ end
225
+ end
226
+
227
+ context 'attachments' do
228
+ before(:each) do
229
+ @client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
230
+ end
231
+
232
+ it 'should not log an error for XML responses' do
233
+ xml = %(<?xml version="1.0" encoding="UTF-8"?>\n<details>some info</details>)
234
+ stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).to_return(body: xml)
235
+ expect_log('Sending GET request to api.itrp.com:443/v1/me', :debug)
236
+ expect_log("XML response:\n#{xml}", :debug)
237
+ response = @client.get('me')
238
+ expect(response.valid?).to be_falsey
239
+ expect(response.raw.body).to eq(xml)
240
+ end
241
+
242
+ it 'should not log an error for redirects' do
243
+ stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).to_return(body: '', status: 303, headers: {'Location' => 'http://redirect.example.com/to/here'})
244
+ expect_log('Sending GET request to api.itrp.com:443/v1/me', :debug)
245
+ expect_log('Redirect: http://redirect.example.com/to/here', :debug)
246
+ response = @client.get('me')
247
+ expect(response.valid?).to be_falsey
248
+ expect(response.raw.body).to be_nil
249
+ end
250
+
251
+ it "should not parse attachments for get requests" do
252
+ expect(Itrp::Attachments).not_to receive(:new)
253
+ stub_request(:get, 'https://api.itrp.com/v1/requests/777?attachments=/tmp/first.png,/tmp/second.zip&note=note').with(basic_auth: ['secret', 'x']).to_return(body: {id: 777, upload_called: false}.to_json)
254
+
255
+ response = @client.get('/requests/777', {note: 'note', attachments: ['/tmp/first.png', '/tmp/second.zip'] })
256
+ expect(response.valid?).to be_truthy
257
+ expect(response[:upload_called]).to be_falsey
258
+ end
259
+
260
+ [:post, :patch].each do |method|
261
+ it "should parse attachments for #{method} requests" do
262
+ attachments = double('Itrp::Attachments')
263
+ expect(attachments).to receive(:upload_attachments!) do |path, data|
264
+ expect(path).to eq '/requests/777'
265
+ expect(data[:attachments]).to eq ['/tmp/first.png', '/tmp/second.zip']
266
+ data.delete(:attachments)
267
+ data[:note_attachments] = 'processed'
268
+ end
269
+ expect(Itrp::Attachments).to receive(:new).with(@client){ attachments }
270
+ stub_request(method, 'https://api.itrp.com/v1/requests/777').with(basic_auth: ['secret', 'x']).with(body: {note: 'note', note_attachments: 'processed' }).to_return(body: {id: 777, upload_called: true}.to_json)
271
+
272
+ response = @client.send(method, '/requests/777', {note: 'note', attachments: ['/tmp/first.png', '/tmp/second.zip'] })
273
+ expect(response.valid?).to be_truthy
274
+ expect(response[:upload_called]).to be_truthy
275
+ end
276
+ end
277
+
278
+ end
279
+
280
+ context 'import' do
281
+ before(:each) do
282
+ @client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
283
+ @multi_part_body = "--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\npeople\r\n--0123456789ABLEWASIEREISAWELBA9876543210\r\nContent-Disposition: form-data; name=\"file\"; filename=\"#{@fixture_dir}/people.csv\"\r\nContent-Type: text/comma-separated-values\r\n\r\nPrimary Email,Name\nchess.cole@example.com,Chess Cole\ned.turner@example.com,Ed Turner\r\n--0123456789ABLEWASIEREISAWELBA9876543210--"
284
+ @multi_part_headers = {'Accept'=>'*/*', 'Content-Type'=>'multipart/form-data; boundary=0123456789ABLEWASIEREISAWELBA9876543210', 'User-Agent'=>'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6'}
285
+
286
+ @import_queued_response = {body: {state: 'queued'}.to_json}
287
+ @import_processing_response = {body: {state: 'processing'}.to_json}
288
+ @import_done_response = {body: {state: 'done', results: {errors: 0, updated: 1, created: 1, failures: 0, unchanged: 0, deleted: 0}}.to_json}
289
+ @import_failed_response = {body: {state: 'error', message: 'Invalid byte sequence in UTF-8 on line 2', results: {errors: 1, updated: 1, created: 0, failures: 1, unchanged: 0, deleted: 0}}.to_json}
290
+ @server_failed_response = {body: {state: 'error', message: 'Invalid byte sequence in UTF-8 on line 2', results: {errors: 1, updated: 1, created: 0, failures: 1, unchanged: 0, deleted: 0}}.to_json}
291
+ allow(@client).to receive(:sleep)
292
+ end
293
+
294
+ it 'should import a CSV file' do
295
+ stub_request(:post, 'https://api.itrp.com/v1/import').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {token: '68ef5ef0f64c0'}.to_json)
296
+ expect_log("Import file '#{@fixture_dir}/people.csv' successfully uploaded with token '68ef5ef0f64c0'.")
297
+
298
+ response = @client.import(File.new("#{@fixture_dir}/people.csv"), 'people')
299
+ expect(response[:token]).to eq('68ef5ef0f64c0')
300
+ end
301
+
302
+ it 'should import a CSV file by filename' do
303
+ stub_request(:post, 'https://api.itrp.com/v1/import').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {token: '68ef5ef0f64c0'}.to_json)
304
+ response = @client.import("#{@fixture_dir}/people.csv", 'people')
305
+ expect(response[:token]).to eq('68ef5ef0f64c0')
306
+ end
307
+
308
+ it 'should wait for the import to complete' do
309
+ stub_request(:post, 'https://api.itrp.com/v1/import').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {token: '68ef5ef0f64c0'}.to_json)
310
+ progress_stub = stub_request(:get, 'https://api.itrp.com/v1/import/68ef5ef0f64c0').with(basic_auth: ['secret', 'x']).to_return(@import_queued_response, @import_processing_response, @import_done_response)
311
+
312
+ # verify the correct log statement are made
313
+ expect_log('Sending POST request to api.itrp.com:443/v1/import', :debug)
314
+ expect_log("Response:\n{\n \"token\": \"68ef5ef0f64c0\"\n}", :debug)
315
+ expect_log("Import file '#{@fixture_dir}/people.csv' successfully uploaded with token '68ef5ef0f64c0'.")
316
+ expect_log('Sending GET request to api.itrp.com:443/v1/import/68ef5ef0f64c0', :debug)
317
+ expect_log("Response:\n{\n \"state\": \"queued\"\n}", :debug)
318
+ expect_log("Import of '#{@fixture_dir}/people.csv' is queued. Checking again in 30 seconds.", :debug)
319
+ expect_log('Sending GET request to api.itrp.com:443/v1/import/68ef5ef0f64c0', :debug)
320
+ expect_log("Response:\n{\n \"state\": \"processing\"\n}", :debug)
321
+ expect_log("Import of '#{@fixture_dir}/people.csv' is processing. Checking again in 30 seconds.", :debug)
322
+ expect_log('Sending GET request to api.itrp.com:443/v1/import/68ef5ef0f64c0', :debug)
323
+ expect_log("Response:\n{\n \"state\": \"done\",\n \"results\": {\n \"errors\": 0,\n \"updated\": 1,\n \"created\": 1,\n \"failures\": 0,\n \"unchanged\": 0,\n \"deleted\": 0\n }\n}", :debug)
324
+
325
+ response = @client.import("#{@fixture_dir}/people.csv", 'people', true)
326
+ expect(response[:state]).to eq('done')
327
+ expect(response[:results][:updated]).to eq(1)
328
+ expect(progress_stub).to have_been_requested.times(3)
329
+ end
330
+
331
+ it 'should wait for the import to fail' do
332
+ stub_request(:post, 'https://api.itrp.com/v1/import').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {token: '68ef5ef0f64c0'}.to_json)
333
+ progress_stub = stub_request(:get, 'https://api.itrp.com/v1/import/68ef5ef0f64c0').with(basic_auth: ['secret', 'x']).to_return(@import_queued_response, @import_processing_response, @import_failed_response)
334
+
335
+ expect{ @client.import("#{@fixture_dir}/people.csv", 'people', true) }.to raise_error(Itrp::Exception, "Unable to monitor progress for people import. Invalid byte sequence in UTF-8 on line 2")
336
+ expect(progress_stub).to have_been_requested.times(3)
337
+ end
338
+
339
+ it 'should not continue when there is an error connecting to ITRP' do
340
+ stub_request(:post, 'https://api.itrp.com/v1/import').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {token: '68ef5ef0f64c0'}.to_json)
341
+ progress_stub = stub_request(:get, 'https://api.itrp.com/v1/import/68ef5ef0f64c0').with(basic_auth: ['secret', 'x']).to_return(@import_queued_response, @import_processing_response).then.to_raise(StandardError.new('network error'))
342
+
343
+ expect{ @client.import("#{@fixture_dir}/people.csv", 'people', true) }.to raise_error(Itrp::Exception, "Unable to monitor progress for people import. 500: No Response from Server - network error for 'api.itrp.com:443/v1/import/68ef5ef0f64c0'")
344
+ expect(progress_stub).to have_been_requested.times(3)
345
+ end
346
+
347
+ it 'should return an invalid response in case waiting for progress is false' do
348
+ stub_request(:post, 'https://api.itrp.com/v1/import').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {message: 'oops!'}.to_json)
349
+ response = @client.import("#{@fixture_dir}/people.csv", 'people', false)
350
+ expect(response.valid?).to be_falsey
351
+ expect(response.message).to eq('oops!')
352
+ end
353
+
354
+ it 'should raise an UploadFailed exception in case waiting for progress is true' do
355
+ stub_request(:post, 'https://api.itrp.com/v1/import').with(basic_auth: ['secret', 'x']).with(body: @multi_part_body, headers: @multi_part_headers).to_return(body: {message: 'oops!'}.to_json)
356
+ expect{ @client.import("#{@fixture_dir}/people.csv", 'people', true) }.to raise_error(Itrp::UploadFailed, 'Failed to queue people import. oops!')
357
+ end
358
+
359
+ end
360
+
361
+
362
+ context 'export' do
363
+ before(:each) do
364
+ @client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
365
+
366
+ @export_queued_response = {body: {state: 'queued'}.to_json}
367
+ @export_processing_response = {body: {state: 'processing'}.to_json}
368
+ @export_done_response = {body: {state: 'done', url: 'https://download.example.com/export.zip?AWSAccessKeyId=12345'}.to_json}
369
+ allow(@client).to receive(:sleep)
370
+ end
371
+
372
+ it 'should export multiple types' do
373
+ stub_request(:post, 'https://api.itrp.com/v1/export').with(basic_auth: ['secret', 'x']).with(body: {type: 'people,people_contact_details'}).to_return(body: {token: '68ef5ef0f64c0'}.to_json)
374
+ expect_log("Export for 'people,people_contact_details' successfully queued with token '68ef5ef0f64c0'.")
375
+
376
+ response = @client.export(['people', 'people_contact_details'])
377
+ expect(response[:token]).to eq('68ef5ef0f64c0')
378
+ end
379
+
380
+ it 'should indicate when nothing is exported' do
381
+ stub_request(:post, 'https://api.itrp.com/v1/export').with(basic_auth: ['secret', 'x']).with(body: {type: 'people', from: '2012-03-30T23:00:00+00:00'}).to_return(status: 204)
382
+ expect_log("No changed records for 'people' since 2012-03-30T23:00:00+00:00.")
383
+
384
+ response = @client.export('people', DateTime.new(2012,03,30,23,00,00))
385
+ expect(response[:token]).to be_nil
386
+ end
387
+
388
+ it 'should export since a certain time' do
389
+ stub_request(:post, 'https://api.itrp.com/v1/export').with(basic_auth: ['secret', 'x']).with(body: {type: 'people', from: '2012-03-30T23:00:00+00:00'}).to_return(body: {token: '68ef5ef0f64c0'}.to_json)
390
+ expect_log("Export for 'people' successfully queued with token '68ef5ef0f64c0'.")
391
+
392
+ response = @client.export('people', DateTime.new(2012,03,30,23,00,00))
393
+ expect(response[:token]).to eq('68ef5ef0f64c0')
394
+ end
395
+
396
+ it 'should wait for the export to complete' do
397
+ stub_request(:post, 'https://api.itrp.com/v1/export').with(basic_auth: ['secret', 'x']).with(body: {type: 'people'}).to_return(body: {token: '68ef5ef0f64c0'}.to_json)
398
+ progress_stub = stub_request(:get, 'https://api.itrp.com/v1/export/68ef5ef0f64c0').with(basic_auth: ['secret', 'x']).to_return(@export_queued_response, @export_processing_response, @export_done_response)
399
+
400
+ # verify the correct log statement are made
401
+ expect_log('Sending POST request to api.itrp.com:443/v1/export', :debug)
402
+ expect_log(%(Response:\n{\n "token": "68ef5ef0f64c0"\n}), :debug)
403
+ expect_log("Export for 'people' successfully queued with token '68ef5ef0f64c0'.")
404
+ expect_log('Sending GET request to api.itrp.com:443/v1/export/68ef5ef0f64c0', :debug)
405
+ expect_log(%(Response:\n{\n "state": "queued"\n}), :debug)
406
+ expect_log("Export of 'people' is queued. Checking again in 30 seconds.", :debug)
407
+ expect_log('Sending GET request to api.itrp.com:443/v1/export/68ef5ef0f64c0', :debug)
408
+ expect_log(%(Response:\n{\n "state": "processing"\n}), :debug)
409
+ expect_log("Export of 'people' is processing. Checking again in 30 seconds.", :debug)
410
+ expect_log('Sending GET request to api.itrp.com:443/v1/export/68ef5ef0f64c0', :debug)
411
+ expect_log(%(Response:\n{\n "state": "done",\n "url": "https://download.example.com/export.zip?AWSAccessKeyId=12345"\n}), :debug)
412
+
413
+ response = @client.export('people', nil, true)
414
+ expect(response[:state]).to eq('done')
415
+ expect(response[:url]).to eq('https://download.example.com/export.zip?AWSAccessKeyId=12345')
416
+ expect(progress_stub).to have_been_requested.times(3)
417
+ end
418
+
419
+ it 'should not continue when there is an error connecting to ITRP' do
420
+ stub_request(:post, 'https://api.itrp.com/v1/export').with(basic_auth: ['secret', 'x']).with(body: {type: 'people'}).to_return(body: {token: '68ef5ef0f64c0'}.to_json)
421
+ progress_stub = stub_request(:get, 'https://api.itrp.com/v1/export/68ef5ef0f64c0').with(basic_auth: ['secret', 'x']).to_return(@export_queued_response, @export_processing_response).then.to_raise(StandardError.new('network error'))
422
+
423
+ expect{ @client.export('people', nil, true) }.to raise_error(Itrp::Exception, "Unable to monitor progress for 'people' export. 500: No Response from Server - network error for 'api.itrp.com:443/v1/export/68ef5ef0f64c0'")
424
+ expect(progress_stub).to have_been_requested.times(3)
425
+ end
426
+
427
+ it 'should return an invalid response in case waiting for progress is false' do
428
+ stub_request(:post, 'https://api.itrp.com/v1/export').with(basic_auth: ['secret', 'x']).with(body: {type: 'people'}).to_return(body: {message: 'oops!'}.to_json)
429
+ response = @client.export('people')
430
+ expect(response.valid?).to be_falsey
431
+ expect(response.message).to eq('oops!')
432
+ end
433
+
434
+ it 'should raise an UploadFailed exception in case waiting for progress is true' do
435
+ stub_request(:post, 'https://api.itrp.com/v1/export').with(basic_auth: ['secret', 'x']).with(body: {type: 'people'}).to_return(body: {message: 'oops!'}.to_json)
436
+ expect{ @client.export('people', nil, true) }.to raise_error(Itrp::UploadFailed, "Failed to queue 'people' export. oops!")
437
+ end
438
+
439
+ end
440
+
441
+ context 'retry' do
442
+ it 'should not retry when max_retry_time = -1' do
443
+ stub = stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).to_raise(StandardError.new('network error'))
444
+ expect_log('Sending GET request to api.itrp.com:443/v1/me', :debug )
445
+ expect_log("Request failed: 500: No Response from Server - network error for 'api.itrp.com:443/v1/me'", :error)
446
+
447
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1)
448
+ response = client.get('me')
449
+ expect(stub).to have_been_requested.times(1)
450
+ expect(response.valid?).to be_falsey
451
+ expect(response.message).to eq("500: No Response from Server - network error for 'api.itrp.com:443/v1/me'")
452
+ end
453
+
454
+ it 'should not retry 4 times when max_retry_time = 16' do
455
+ stub = stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).to_raise(StandardError.new('network error'))
456
+ [2,4,8,16].each_with_index do |secs, i|
457
+ expect_log('Sending GET request to api.itrp.com:443/v1/me', :debug )
458
+ expect_log("Request failed, retry ##{i+1} in #{secs} seconds: 500: No Response from Server - network error for 'api.itrp.com:443/v1/me'", :warn)
459
+ end
460
+
461
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: 16)
462
+ allow(client).to receive(:sleep)
463
+ response = client.get('me')
464
+ expect(stub).to have_been_requested.times(4)
465
+ expect(response.valid?).to be_falsey
466
+ expect(response.message).to eq("500: No Response from Server - network error for 'api.itrp.com:443/v1/me'")
467
+ end
468
+
469
+ it 'should return the response after retry succeeds' do
470
+ stub = stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).to_raise(StandardError.new('network error')).then.to_return(body: {name: 'my name'}.to_json)
471
+ expect_log('Sending GET request to api.itrp.com:443/v1/me', :debug )
472
+ expect_log("Request failed, retry #1 in 2 seconds: 500: No Response from Server - network error for 'api.itrp.com:443/v1/me'", :warn)
473
+ expect_log('Sending GET request to api.itrp.com:443/v1/me', :debug )
474
+ expect_log(%(Response:\n{\n "name": "my name"\n}), :debug )
475
+
476
+ client = Itrp::Client.new(api_token: 'secret', max_retry_time: 16)
477
+ allow(client).to receive(:sleep)
478
+ response = client.get('me')
479
+ expect(stub).to have_been_requested.times(2)
480
+ expect(response.valid?).to be_truthy
481
+ expect(response[:name]).to eq('my name')
482
+ end
483
+ end
484
+
485
+ context 'rate limiting' do
486
+ it 'should not block on rate limit when block_at_rate_limit is false' do
487
+ stub = stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).to_return(status: 429, body: {message: 'Too Many Requests'}.to_json)
488
+ expect_log('Sending GET request to api.itrp.com:443/v1/me', :debug )
489
+ expect_log("Request failed: 429: Too Many Requests", :error)
490
+
491
+ client = Itrp::Client.new(api_token: 'secret', block_at_rate_limit: false)
492
+ response = client.get('me')
493
+ expect(stub).to have_been_requested.times(1)
494
+ expect(response.valid?).to be_falsey
495
+ expect(response.message).to eq('429: Too Many Requests')
496
+ end
497
+
498
+ it 'should block on rate limit when block_at_rate_limit is true' do
499
+ stub = stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).to_return(status: 429, body: {message: 'Too Many Requests'}.to_json).then.to_return(body: {name: 'my name'}.to_json)
500
+ expect_log('Sending GET request to api.itrp.com:443/v1/me', :debug )
501
+ expect_log('Request throttled, trying again in 5 minutes: 429: Too Many Requests', :warn)
502
+ expect_log('Sending GET request to api.itrp.com:443/v1/me', :debug )
503
+ expect_log(%(Response:\n{\n "name": "my name"\n}), :debug )
504
+
505
+ client = Itrp::Client.new(api_token: 'secret', block_at_rate_limit: true)
506
+ allow(client).to receive(:sleep)
507
+ response = client.get('me')
508
+ expect(stub).to have_been_requested.times(2)
509
+ expect(response.valid?).to be_truthy
510
+ expect(response[:name]).to eq('my name')
511
+ end
512
+ end
513
+
514
+ context 'logger' do
515
+ before(:each) do
516
+ @logger = Logger.new(STDOUT)
517
+ @client = Itrp::Client.new(api_token: 'secret', max_retry_time: -1, logger: @logger)
518
+ end
519
+
520
+ it 'should be possible to override the default logger' do
521
+ stub_request(:get, 'https://api.itrp.com/v1/me').with(basic_auth: ['secret', 'x']).to_return(body: {name: 'my name'}.to_json)
522
+ expect_log('Sending GET request to api.itrp.com:443/v1/me', :debug, @logger )
523
+ expect_log(%(Response:\n{\n "name": "my name"\n}), :debug, @logger )
524
+ @client.get('me')
525
+ end
526
+ end
527
+ end