files.com 1.1.768 → 1.1.769
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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +47 -7
- data/_VERSION +1 -1
- data/docs/file.md +37 -0
- data/lib/files.com/api.rb +9 -4
- data/lib/files.com/api_client.rb +181 -55
- data/lib/files.com/list.rb +98 -66
- data/lib/files.com/models/action_log.rb +2 -2
- data/lib/files.com/models/action_notification_export.rb +3 -3
- data/lib/files.com/models/action_notification_export_result.rb +2 -2
- data/lib/files.com/models/ai_assistant_personality.rb +10 -8
- data/lib/files.com/models/ai_task.rb +10 -8
- data/lib/files.com/models/api_key.rb +12 -10
- data/lib/files.com/models/api_request_log.rb +2 -2
- data/lib/files.com/models/app.rb +2 -2
- data/lib/files.com/models/as2_incoming_message.rb +2 -2
- data/lib/files.com/models/as2_outgoing_message.rb +2 -2
- data/lib/files.com/models/as2_partner.rb +10 -8
- data/lib/files.com/models/as2_station.rb +10 -8
- data/lib/files.com/models/automation.rb +12 -10
- data/lib/files.com/models/automation_log.rb +2 -2
- data/lib/files.com/models/automation_run.rb +6 -6
- data/lib/files.com/models/bandwidth_snapshot.rb +2 -2
- data/lib/files.com/models/behavior.rb +12 -10
- data/lib/files.com/models/bundle.rb +10 -8
- data/lib/files.com/models/bundle_action.rb +2 -2
- data/lib/files.com/models/bundle_download.rb +2 -2
- data/lib/files.com/models/bundle_notification.rb +10 -8
- data/lib/files.com/models/bundle_recipient.rb +4 -4
- data/lib/files.com/models/bundle_registration.rb +2 -2
- data/lib/files.com/models/chat_session.rb +3 -3
- data/lib/files.com/models/child_site_management_policy.rb +10 -8
- data/lib/files.com/models/clickwrap.rb +10 -8
- data/lib/files.com/models/custom_domain.rb +11 -9
- data/lib/files.com/models/desktop_configuration_profile.rb +10 -8
- data/lib/files.com/models/dns_record.rb +2 -2
- data/lib/files.com/models/email_incoming_message.rb +2 -2
- data/lib/files.com/models/email_log.rb +2 -2
- data/lib/files.com/models/event_channel.rb +10 -8
- data/lib/files.com/models/event_delivery_attempt.rb +3 -3
- data/lib/files.com/models/event_record.rb +3 -3
- data/lib/files.com/models/event_subscription.rb +10 -8
- data/lib/files.com/models/event_target.rb +10 -8
- data/lib/files.com/models/exavault_api_request_log.rb +2 -2
- data/lib/files.com/models/expectation.rb +11 -9
- data/lib/files.com/models/expectation_evaluation.rb +3 -3
- data/lib/files.com/models/expectation_incident.rb +6 -6
- data/lib/files.com/models/external_event.rb +5 -5
- data/lib/files.com/models/file.rb +23 -15
- data/lib/files.com/models/file_comment.rb +9 -7
- data/lib/files.com/models/file_comment_reaction.rb +2 -2
- data/lib/files.com/models/file_migration.rb +1 -1
- data/lib/files.com/models/file_migration_log.rb +2 -2
- data/lib/files.com/models/folder.rb +3 -3
- data/lib/files.com/models/form_field_set.rb +10 -8
- data/lib/files.com/models/ftp_action_log.rb +2 -2
- data/lib/files.com/models/gpg_key.rb +10 -8
- data/lib/files.com/models/group.rb +10 -8
- data/lib/files.com/models/group_user.rb +9 -7
- data/lib/files.com/models/history.rb +10 -10
- data/lib/files.com/models/history_export.rb +3 -3
- data/lib/files.com/models/history_export_result.rb +2 -2
- data/lib/files.com/models/holiday_calendar.rb +10 -8
- data/lib/files.com/models/holiday_region.rb +2 -2
- data/lib/files.com/models/inbound_s3_log.rb +2 -2
- data/lib/files.com/models/inbox_recipient.rb +4 -4
- data/lib/files.com/models/inbox_registration.rb +2 -2
- data/lib/files.com/models/inbox_upload.rb +2 -2
- data/lib/files.com/models/integration_centric_profile.rb +10 -8
- data/lib/files.com/models/invoice.rb +3 -3
- data/lib/files.com/models/ip_address.rb +8 -8
- data/lib/files.com/models/key_lifecycle_rule.rb +10 -8
- data/lib/files.com/models/lock.rb +3 -3
- data/lib/files.com/models/metadata_category.rb +12 -10
- data/lib/files.com/models/notification.rb +10 -8
- data/lib/files.com/models/outbound_connection_log.rb +2 -2
- data/lib/files.com/models/partner.rb +10 -8
- data/lib/files.com/models/partner_channel.rb +10 -8
- data/lib/files.com/models/partner_channel_template.rb +10 -8
- data/lib/files.com/models/partner_site_request.rb +4 -4
- data/lib/files.com/models/payment.rb +3 -3
- data/lib/files.com/models/pending_work_event.rb +3 -3
- data/lib/files.com/models/permission.rb +4 -4
- data/lib/files.com/models/public_hosting_request_log.rb +2 -2
- data/lib/files.com/models/public_key.rb +10 -8
- data/lib/files.com/models/remote_bandwidth_snapshot.rb +2 -2
- data/lib/files.com/models/remote_mount_backend.rb +10 -8
- data/lib/files.com/models/remote_server.rb +13 -11
- data/lib/files.com/models/remote_server_credential.rb +10 -8
- data/lib/files.com/models/request.rb +6 -6
- data/lib/files.com/models/restore.rb +4 -4
- data/lib/files.com/models/schedule.rb +10 -8
- data/lib/files.com/models/scheduled_export.rb +10 -8
- data/lib/files.com/models/scim_log.rb +3 -3
- data/lib/files.com/models/secret.rb +10 -8
- data/lib/files.com/models/session.rb +2 -2
- data/lib/files.com/models/settings_change.rb +2 -2
- data/lib/files.com/models/sftp_action_log.rb +2 -2
- data/lib/files.com/models/sftp_host_key.rb +10 -8
- data/lib/files.com/models/share_group.rb +10 -8
- data/lib/files.com/models/siem_http_destination.rb +10 -8
- data/lib/files.com/models/siem_http_destination_event.rb +3 -3
- data/lib/files.com/models/site.rb +3 -3
- data/lib/files.com/models/site_subdomain_redirect.rb +3 -3
- data/lib/files.com/models/snapshot.rb +10 -8
- data/lib/files.com/models/sso_event.rb +3 -3
- data/lib/files.com/models/sso_strategy.rb +3 -3
- data/lib/files.com/models/style.rb +6 -5
- data/lib/files.com/models/sync.rb +10 -8
- data/lib/files.com/models/sync_log.rb +2 -2
- data/lib/files.com/models/sync_run.rb +3 -3
- data/lib/files.com/models/usage_daily_snapshot.rb +2 -2
- data/lib/files.com/models/usage_snapshot.rb +2 -2
- data/lib/files.com/models/user.rb +10 -8
- data/lib/files.com/models/user_additional_email_recipient.rb +10 -8
- data/lib/files.com/models/user_cipher_use.rb +2 -2
- data/lib/files.com/models/user_lifecycle_rule.rb +10 -8
- data/lib/files.com/models/user_request.rb +5 -5
- data/lib/files.com/models/user_security_event.rb +3 -3
- data/lib/files.com/models/user_sftp_client_use.rb +2 -2
- data/lib/files.com/models/web_dav_action_log.rb +2 -2
- data/lib/files.com/models/webhook_test.rb +2 -2
- data/lib/files.com/models/workspace.rb +10 -8
- data/lib/files.com/response.rb +24 -0
- data/lib/files.com/sizable_io.rb +65 -4
- data/lib/files.com/version.rb +1 -1
- data/lib/files.com.rb +23 -1
- data/spec/language_spec.rb +20 -10
- data/spec/lib/api_client_spec.rb +173 -18
- data/spec/list_spec.rb +203 -187
- data/spec/models/file_transfer_spec.rb +346 -0
- data/spec/models/persistence_spec.rb +167 -0
- data/spec/spec_helper.rb +122 -6
- data/spec/support/stubbed_api.rb +14 -0
- data/spec/support/transfer_server.rb +60 -0
- metadata +5 -1
data/lib/files.com.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "json"
|
|
|
7
7
|
require "logger"
|
|
8
8
|
require "openssl"
|
|
9
9
|
require "rbconfig"
|
|
10
|
+
require "rbconfig/sizeof"
|
|
10
11
|
require "securerandom"
|
|
11
12
|
require "set"
|
|
12
13
|
require "socket"
|
|
@@ -201,7 +202,8 @@ module Files
|
|
|
201
202
|
@read_timeout = 60
|
|
202
203
|
|
|
203
204
|
class << self
|
|
204
|
-
attr_accessor :api_key, :base_url, :default_headers, :initial_network_retry_delay, :language, :max_network_retry_delay, :
|
|
205
|
+
attr_accessor :api_key, :base_url, :default_headers, :initial_network_retry_delay, :language, :max_network_retry_delay, :proxy, :session_id, :workspace_id
|
|
206
|
+
attr_reader :open_timeout, :read_timeout
|
|
205
207
|
end
|
|
206
208
|
|
|
207
209
|
# map to the same values as the standard library's logger
|
|
@@ -259,6 +261,26 @@ module Files
|
|
|
259
261
|
@max_network_retries = val.to_i
|
|
260
262
|
end
|
|
261
263
|
|
|
264
|
+
# Net::HTTP hands each timeout to Ruby to wait on. CRuby converts it to the
|
|
265
|
+
# platform's time_t and raises RangeError for more seconds than that holds,
|
|
266
|
+
# partway through a request it may already have sent. JRuby takes any number.
|
|
267
|
+
MAX_TIMEOUT = RUBY_PLATFORM == "java" ? Float::INFINITY : (2**((8 * RbConfig::SIZEOF["time_t"]) - 1)) - 1
|
|
268
|
+
private_constant :MAX_TIMEOUT
|
|
269
|
+
|
|
270
|
+
def self.open_timeout=(seconds)
|
|
271
|
+
@open_timeout = checked_timeout("open_timeout", seconds)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def self.read_timeout=(seconds)
|
|
275
|
+
@read_timeout = checked_timeout("read_timeout", seconds)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
private_class_method def self.checked_timeout(name, seconds)
|
|
279
|
+
return seconds if seconds.nil? || (seconds.is_a?(Numeric) && seconds.real? && seconds >= 0 && seconds <= MAX_TIMEOUT)
|
|
280
|
+
|
|
281
|
+
raise ArgumentError, "#{name} should be nil or a number of seconds from 0 to #{MAX_TIMEOUT}"
|
|
282
|
+
end
|
|
283
|
+
|
|
262
284
|
def self.session=(session)
|
|
263
285
|
session.save unless session.id
|
|
264
286
|
self.session_id = session.id
|
data/spec/language_spec.rb
CHANGED
|
@@ -9,20 +9,30 @@ RSpec.describe "Language setting" do
|
|
|
9
9
|
Files.language = old_language
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
describe "request headers" do
|
|
13
|
+
let(:client) { Files::ApiClient.new }
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
it "sends the configured language as the Accept-Language header" do
|
|
16
|
+
Files.language = "es"
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
headers = client.send(:request_headers, "api-key", nil, :get, nil)
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
expect(Files.language).to eq("es")
|
|
21
|
+
expect(headers["Accept-Language"]).to eq("es")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "omits the Accept-Language header when unset" do
|
|
25
|
+
headers = client.send(:request_headers, "api-key", nil, :get, nil)
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
expect(headers).not_to have_key("Accept-Language")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
describe "API errors", :with_test_folder do
|
|
32
|
+
it "gets API errors in the language it sets" do
|
|
33
|
+
Files.language = "es"
|
|
34
|
+
expect(Files.language).to eq("es")
|
|
35
|
+
expect { Files::File.find(test_folder.join("read.txt").to_s, {}, options) }.to raise_error(Files::NotFoundError, a_string_including("No se ha encontrado"))
|
|
36
|
+
end
|
|
27
37
|
end
|
|
28
38
|
end
|
data/spec/lib/api_client_spec.rb
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
require "spec_helper"
|
|
2
|
+
require_relative "../support/stubbed_api"
|
|
2
3
|
|
|
3
4
|
RSpec.describe Files::ApiClient do
|
|
5
|
+
include StubbedApi
|
|
6
|
+
|
|
4
7
|
let(:subject) { described_class.new }
|
|
5
8
|
|
|
6
9
|
describe "signed transfer privacy" do
|
|
7
10
|
it "keeps API error types without echoing a transfer URL" do
|
|
8
11
|
url = "https://transfer.example.test/private-part?signature=secret"
|
|
9
|
-
output = StringIO.new
|
|
10
12
|
original_logger = Files.logger
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
|
|
14
|
+
{ { type: "not-found", error: url } => Files::NotFoundError, { error: url } => Files::APIError }.each do |body, error_class|
|
|
15
|
+
output = StringIO.new
|
|
16
|
+
Files.logger = Logger.new(output, level: Logger::INFO)
|
|
17
|
+
connection = Faraday.new do |builder|
|
|
18
|
+
builder.response :raise_error
|
|
19
|
+
builder.adapter :test do |stub|
|
|
20
|
+
stub.put(url) { [ 404, {}, body.to_json ] }
|
|
21
|
+
end
|
|
16
22
|
end
|
|
17
|
-
end
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
expect { described_class.new(connection).remote_request(:put, url, {}, "part") }.to raise_error(error_class) { |error|
|
|
25
|
+
expect(error.http_status).to eq(404)
|
|
26
|
+
expect(error.full_message).not_to include(url, "private-part", "secret")
|
|
27
|
+
}
|
|
28
|
+
expect(output.string).not_to include(url, "private-part", "secret")
|
|
29
|
+
end
|
|
24
30
|
ensure
|
|
25
31
|
Files.logger = original_logger
|
|
26
32
|
end
|
|
@@ -64,6 +70,69 @@ RSpec.describe Files::ApiClient do
|
|
|
64
70
|
end
|
|
65
71
|
end
|
|
66
72
|
|
|
73
|
+
describe "signed URLs quoted by API responses" do
|
|
74
|
+
signed_url = "https://transfer.example.test/object?X-Amz-Signature=PRIVATE-SENTINEL"
|
|
75
|
+
password_url = "https://user:private'PRIVATE-SENTINEL@bucket.example/key.txt?token=PRIVATE-SENTINEL#PRIVATE-SENTINEL"
|
|
76
|
+
path_url = "https://bucket.example/O'Brien/key.txt?token=PRIVATE-SENTINEL#PRIVATE-SENTINEL"
|
|
77
|
+
long_scheme = "a" * 32
|
|
78
|
+
|
|
79
|
+
around do |example|
|
|
80
|
+
original = [ Files.logger, Files.max_network_retries ]
|
|
81
|
+
Files.max_network_retries = 0
|
|
82
|
+
example.run
|
|
83
|
+
ensure
|
|
84
|
+
Files.logger, Files.max_network_retries = original
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The error File.find raises for this response, and what the logger wrote.
|
|
88
|
+
def find_file_answering(status, body, log_level)
|
|
89
|
+
output = StringIO.new
|
|
90
|
+
Files.logger = Logger.new(output, level: log_level)
|
|
91
|
+
client = stubbed_api_client do |stub|
|
|
92
|
+
stub.get("/api/rest/v1/file_actions/metadata/object") { [ status, { "Content-Type" => "application/json", "X-Request-Id" => "request-1" }, body ] }
|
|
93
|
+
end
|
|
94
|
+
Files::File.find("object", {}, api_key: "key", client: client)
|
|
95
|
+
raise "the request succeeded"
|
|
96
|
+
rescue Files::Error => e
|
|
97
|
+
[ e, output.string ]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
"an error object without a type" => [ 422, { error: "Could not reach #{signed_url}" }.to_json, Files::APIError, "Could not reach https://transfer.example.test/object?[redacted]" ],
|
|
102
|
+
"a typed error object" => [ 422, { type: "not-found", error: "Could not reach #{signed_url}" }.to_json, Files::NotFoundError, "Could not reach https://transfer.example.test/object?[redacted]" ],
|
|
103
|
+
"a successful response that is not JSON" => [ 200, "{\"download_uri\":\"#{signed_url}", Files::APIConnectionError, /not valid JSON \(HTTP response code was 200\)/ ],
|
|
104
|
+
"a URL with an apostrophe in its password" => [ 422, { error: "Could not reach #{password_url}" }.to_json, Files::APIError, "Could not reach https://[redacted]@bucket.example/key.txt?[redacted]" ],
|
|
105
|
+
"a URL with an apostrophe in its path" => [ 422, { type: "not-found", error: "Could not reach #{path_url}" }.to_json, Files::NotFoundError, "Could not reach https://bucket.example/O'Brien/key.txt?[redacted]" ],
|
|
106
|
+
"a URL quoted with apostrophes" => [ 422, { error: "Could not reach '#{path_url}'." }.to_json, Files::APIError, "Could not reach 'https://bucket.example/O'Brien/key.txt?[redacted]'." ],
|
|
107
|
+
"a URL with a 32-character scheme" => [ 422, { error: "Could not reach #{long_scheme}://user:PRIVATE-SENTINEL@bucket.example/key?token=PRIVATE-SENTINEL#PRIVATE-SENTINEL" }.to_json, Files::APIError, "Could not reach #{long_scheme}://[redacted]@bucket.example/key?[redacted]" ],
|
|
108
|
+
"a URL with an empty authority and a query" => [ 422, { type: "not-found", error: "Could not reach custom://?token=PRIVATE-SENTINEL#PRIVATE-SENTINEL" }.to_json, Files::NotFoundError, "Could not reach custom://?[redacted]" ],
|
|
109
|
+
"a URL with an empty authority and only a fragment" => [ 422, { error: "Could not reach custom://#PRIVATE-SENTINEL" }.to_json, Files::APIError, "Could not reach custom://?[redacted]" ],
|
|
110
|
+
"a quoted URL with nothing after its scheme" => [ 422, { type: "not-found", error: "Could not open 'file://'." }.to_json, Files::NotFoundError, "Could not open 'file://'." ],
|
|
111
|
+
"a URL with an empty authority and an @ in its path" => [ 422, { error: "Could not open file:///tmp@part" }.to_json, Files::APIError, "Could not open file:///tmp@part" ],
|
|
112
|
+
"adjacent quoted URLs" => [ 422, { type: "not-found", error: "Tried 'https://a.example/x','https://user:PRIVATE-SENTINEL@b.example/y';'https://user:PRIVATE-SENTINEL@c.example/z?next=https://d.example/PRIVATE-SENTINEL'." }.to_json, Files::NotFoundError, "Tried 'https://a.example/x','https://[redacted]@b.example/y';'https://[redacted]@c.example/z?[redacted]'." ],
|
|
113
|
+
}.each do |description, (status, body, error_class, message)|
|
|
114
|
+
it "keeps the credentials out of the error text, its causes and INFO logs for #{description}" do
|
|
115
|
+
error, info_log = find_file_answering(status, body, Logger::INFO)
|
|
116
|
+
|
|
117
|
+
expect(error).to be_an_instance_of(error_class)
|
|
118
|
+
expect(error.message).to(message.is_a?(Regexp) ? match(message) : eq(message))
|
|
119
|
+
expect(error.http_status).to eq(status)
|
|
120
|
+
expect(error.http_headers["x-request-id"]).to eq("request-1")
|
|
121
|
+
expect(error.http_body).to eq(body)
|
|
122
|
+
chain = [ error ]
|
|
123
|
+
chain << chain.last.cause while chain.last.cause
|
|
124
|
+
expect(chain.map { |raised| raised.full_message(highlight: false) }.join).not_to include("PRIVATE-SENTINEL")
|
|
125
|
+
expect(info_log).not_to include("PRIVATE-SENTINEL")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "still writes the unredacted response to the DEBUG log" do
|
|
130
|
+
_error, debug_log = find_file_answering(422, { error: "Could not reach #{signed_url}" }.to_json, Logger::DEBUG)
|
|
131
|
+
|
|
132
|
+
expect(debug_log).to include(signed_url)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
67
136
|
describe "#execute_request_with_rescues" do
|
|
68
137
|
let(:context) { double('context', method: 'some method', path: 'some path') }
|
|
69
138
|
|
|
@@ -72,25 +141,27 @@ RSpec.describe Files::ApiClient do
|
|
|
72
141
|
allow(subject).to receive(:log_request).and_raise(Faraday::ServerError.new('', mock_response))
|
|
73
142
|
end
|
|
74
143
|
|
|
75
|
-
it 'retries with sleeps and raises
|
|
144
|
+
it 'retries with sleeps and then raises' do
|
|
76
145
|
expect(subject).to receive(:sleep).with(0.5).ordered
|
|
77
146
|
expect(subject).to receive(:sleep).with(be_between(0.5, 1)).ordered
|
|
78
147
|
expect(subject).to receive(:sleep).with(be_between(1, 2)).ordered
|
|
79
148
|
expect {
|
|
80
149
|
subject.execute_request_with_rescues(1, context) { 'empty block' }
|
|
81
|
-
}.to raise_error(
|
|
150
|
+
}.to raise_error(error_class, error_message)
|
|
82
151
|
end
|
|
83
152
|
end
|
|
84
153
|
|
|
85
|
-
context 'when response is a
|
|
154
|
+
context 'when response is a Files.com error object' do
|
|
86
155
|
it_behaves_like 'a server error handler' do
|
|
87
|
-
let(:
|
|
156
|
+
let(:error_class) { Files::APIError }
|
|
157
|
+
let(:error_message) { "Server Error" }
|
|
88
158
|
let(:mock_response) { { status: 502, headers: {}, body: { error: "Server Error" }.to_json } }
|
|
89
159
|
end
|
|
90
160
|
end
|
|
91
161
|
|
|
92
162
|
context 'when response is html' do
|
|
93
163
|
it_behaves_like 'a server error handler' do
|
|
164
|
+
let(:error_class) { Files::APIConnectionError }
|
|
94
165
|
let(:mock_response) {
|
|
95
166
|
{
|
|
96
167
|
status: 502,
|
|
@@ -149,11 +220,11 @@ RSpec.describe Files::ApiClient do
|
|
|
149
220
|
end
|
|
150
221
|
end
|
|
151
222
|
|
|
152
|
-
it "throws generic
|
|
223
|
+
it "throws generic api error when no type" do
|
|
153
224
|
allow(subject).to receive(:log_request).and_raise(Faraday::BadRequestError.new('', mock_response_without_type))
|
|
154
225
|
expect {
|
|
155
226
|
subject.execute_request_with_rescues(1, context) { 'empty block' }
|
|
156
|
-
}.to raise_error(Files::
|
|
227
|
+
}.to raise_error(Files::APIError, "Bad Request")
|
|
157
228
|
end
|
|
158
229
|
|
|
159
230
|
it "throws generic bad request error when no body at all" do
|
|
@@ -178,4 +249,88 @@ RSpec.describe Files::ApiClient do
|
|
|
178
249
|
end
|
|
179
250
|
end
|
|
180
251
|
end
|
|
252
|
+
|
|
253
|
+
describe "failed API responses" do
|
|
254
|
+
around do |example|
|
|
255
|
+
original = [ Files.logger, Files.max_network_retries ]
|
|
256
|
+
Files.logger = Logger.new(IO::NULL)
|
|
257
|
+
Files.max_network_retries = 0
|
|
258
|
+
example.run
|
|
259
|
+
ensure
|
|
260
|
+
Files.logger, Files.max_network_retries = original
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def find_workspace_failing_with(status, body)
|
|
264
|
+
client = stubbed_api_client do |stub|
|
|
265
|
+
stub.get("/api/rest/v1/workspaces/1") { [ status, { "Content-Type" => "application/json", "X-Request-Id" => "request-1" }, body ] }
|
|
266
|
+
end
|
|
267
|
+
Files::Workspace.find(1, {}, api_key: "key", client: client)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Each error object and the error it raises, whatever the status.
|
|
271
|
+
error_objects = {
|
|
272
|
+
"a known type" => [ { error: "sentinel", type: "not-found" }, Files::NotFoundError ],
|
|
273
|
+
"an unknown type" => [ { error: "sentinel", type: "made-up-type" }, Files::APIError ],
|
|
274
|
+
"a blank type" => [ { error: "sentinel", type: "" }, Files::APIError ],
|
|
275
|
+
"a non-string type" => [ { error: "sentinel", type: 7 }, Files::APIError ],
|
|
276
|
+
"a null type" => [ { error: "sentinel", type: nil }, Files::APIError ],
|
|
277
|
+
"no type" => [ { error: "sentinel" }, Files::APIError ],
|
|
278
|
+
"an errors list" => [ { errors: [ "sentinel" ] }, Files::APIError ],
|
|
279
|
+
"a type naming an SDK usage error" => [ { error: "sentinel", type: "invalid-parameter" }, Files::APIError ],
|
|
280
|
+
"a type naming a Ruby exception" => [ { error: "sentinel", type: "standard" }, Files::APIError ],
|
|
281
|
+
"a known type without a message" => [ { error: 5, type: "not-found" }, Files::NotFoundError, /without a message/ ],
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# Bodies that are not error objects, so the API gave no usable answer.
|
|
285
|
+
other_bodies = {
|
|
286
|
+
"JSON null" => "null",
|
|
287
|
+
"a JSON array" => [ { error: "sentinel" } ].to_json,
|
|
288
|
+
"a JSON scalar" => "\"sentinel\"",
|
|
289
|
+
"a JSON object without an error" => { status: "sentinel" }.to_json,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
[ 404, 500 ].each do |status|
|
|
293
|
+
context "with HTTP #{status}" do
|
|
294
|
+
error_objects.each do |description, (body, error_class, message)|
|
|
295
|
+
it "raises #{error_class} for an error object with #{description}" do
|
|
296
|
+
expect { find_workspace_failing_with(status, body.to_json) }.to raise_error(error_class) { |error|
|
|
297
|
+
expect(error).to be_an_instance_of(error_class)
|
|
298
|
+
expect(error.message).to match(message || /\Asentinel\z/)
|
|
299
|
+
expect(error.http_status).to eq(status)
|
|
300
|
+
expect(error.http_headers["x-request-id"]).to eq("request-1")
|
|
301
|
+
expect(error.json_body).to eq(body)
|
|
302
|
+
expect(error.response).to be_a(Files::Response)
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
other_bodies.each do |description, body|
|
|
308
|
+
it "raises APIConnectionError for #{description}, without repeating the body" do
|
|
309
|
+
expect { find_workspace_failing_with(status, body) }.to raise_error(Files::APIConnectionError) { |error|
|
|
310
|
+
expect(error).to be_an_instance_of(Files::APIConnectionError)
|
|
311
|
+
expect(error.message).not_to include("sentinel")
|
|
312
|
+
}
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
describe "network timeouts" do
|
|
320
|
+
around do |example|
|
|
321
|
+
original = [ Files.open_timeout, Files.read_timeout ]
|
|
322
|
+
example.run
|
|
323
|
+
ensure
|
|
324
|
+
Files.open_timeout, Files.read_timeout = original
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Net::HTTP would raise RangeError for them partway through a request, after sending it.
|
|
328
|
+
it "refuses timeouts longer than Ruby can wait for when they are set" do
|
|
329
|
+
skip "JRuby can wait that long" if RUBY_PLATFORM == "java"
|
|
330
|
+
|
|
331
|
+
expect { Files.open_timeout = 2**63 }.to raise_error(ArgumentError, /open_timeout/)
|
|
332
|
+
expect { Files.read_timeout = 1e19 }.to raise_error(ArgumentError, /read_timeout/)
|
|
333
|
+
expect([ Files.open_timeout, Files.read_timeout ]).to eq([ 30, 60 ])
|
|
334
|
+
end
|
|
335
|
+
end
|
|
181
336
|
end
|