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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +47 -7
  4. data/_VERSION +1 -1
  5. data/docs/file.md +37 -0
  6. data/lib/files.com/api.rb +9 -4
  7. data/lib/files.com/api_client.rb +181 -55
  8. data/lib/files.com/list.rb +98 -66
  9. data/lib/files.com/models/action_log.rb +2 -2
  10. data/lib/files.com/models/action_notification_export.rb +3 -3
  11. data/lib/files.com/models/action_notification_export_result.rb +2 -2
  12. data/lib/files.com/models/ai_assistant_personality.rb +10 -8
  13. data/lib/files.com/models/ai_task.rb +10 -8
  14. data/lib/files.com/models/api_key.rb +12 -10
  15. data/lib/files.com/models/api_request_log.rb +2 -2
  16. data/lib/files.com/models/app.rb +2 -2
  17. data/lib/files.com/models/as2_incoming_message.rb +2 -2
  18. data/lib/files.com/models/as2_outgoing_message.rb +2 -2
  19. data/lib/files.com/models/as2_partner.rb +10 -8
  20. data/lib/files.com/models/as2_station.rb +10 -8
  21. data/lib/files.com/models/automation.rb +12 -10
  22. data/lib/files.com/models/automation_log.rb +2 -2
  23. data/lib/files.com/models/automation_run.rb +6 -6
  24. data/lib/files.com/models/bandwidth_snapshot.rb +2 -2
  25. data/lib/files.com/models/behavior.rb +12 -10
  26. data/lib/files.com/models/bundle.rb +10 -8
  27. data/lib/files.com/models/bundle_action.rb +2 -2
  28. data/lib/files.com/models/bundle_download.rb +2 -2
  29. data/lib/files.com/models/bundle_notification.rb +10 -8
  30. data/lib/files.com/models/bundle_recipient.rb +4 -4
  31. data/lib/files.com/models/bundle_registration.rb +2 -2
  32. data/lib/files.com/models/chat_session.rb +3 -3
  33. data/lib/files.com/models/child_site_management_policy.rb +10 -8
  34. data/lib/files.com/models/clickwrap.rb +10 -8
  35. data/lib/files.com/models/custom_domain.rb +11 -9
  36. data/lib/files.com/models/desktop_configuration_profile.rb +10 -8
  37. data/lib/files.com/models/dns_record.rb +2 -2
  38. data/lib/files.com/models/email_incoming_message.rb +2 -2
  39. data/lib/files.com/models/email_log.rb +2 -2
  40. data/lib/files.com/models/event_channel.rb +10 -8
  41. data/lib/files.com/models/event_delivery_attempt.rb +3 -3
  42. data/lib/files.com/models/event_record.rb +3 -3
  43. data/lib/files.com/models/event_subscription.rb +10 -8
  44. data/lib/files.com/models/event_target.rb +10 -8
  45. data/lib/files.com/models/exavault_api_request_log.rb +2 -2
  46. data/lib/files.com/models/expectation.rb +11 -9
  47. data/lib/files.com/models/expectation_evaluation.rb +3 -3
  48. data/lib/files.com/models/expectation_incident.rb +6 -6
  49. data/lib/files.com/models/external_event.rb +5 -5
  50. data/lib/files.com/models/file.rb +23 -15
  51. data/lib/files.com/models/file_comment.rb +9 -7
  52. data/lib/files.com/models/file_comment_reaction.rb +2 -2
  53. data/lib/files.com/models/file_migration.rb +1 -1
  54. data/lib/files.com/models/file_migration_log.rb +2 -2
  55. data/lib/files.com/models/folder.rb +3 -3
  56. data/lib/files.com/models/form_field_set.rb +10 -8
  57. data/lib/files.com/models/ftp_action_log.rb +2 -2
  58. data/lib/files.com/models/gpg_key.rb +10 -8
  59. data/lib/files.com/models/group.rb +10 -8
  60. data/lib/files.com/models/group_user.rb +9 -7
  61. data/lib/files.com/models/history.rb +10 -10
  62. data/lib/files.com/models/history_export.rb +3 -3
  63. data/lib/files.com/models/history_export_result.rb +2 -2
  64. data/lib/files.com/models/holiday_calendar.rb +10 -8
  65. data/lib/files.com/models/holiday_region.rb +2 -2
  66. data/lib/files.com/models/inbound_s3_log.rb +2 -2
  67. data/lib/files.com/models/inbox_recipient.rb +4 -4
  68. data/lib/files.com/models/inbox_registration.rb +2 -2
  69. data/lib/files.com/models/inbox_upload.rb +2 -2
  70. data/lib/files.com/models/integration_centric_profile.rb +10 -8
  71. data/lib/files.com/models/invoice.rb +3 -3
  72. data/lib/files.com/models/ip_address.rb +8 -8
  73. data/lib/files.com/models/key_lifecycle_rule.rb +10 -8
  74. data/lib/files.com/models/lock.rb +3 -3
  75. data/lib/files.com/models/metadata_category.rb +12 -10
  76. data/lib/files.com/models/notification.rb +10 -8
  77. data/lib/files.com/models/outbound_connection_log.rb +2 -2
  78. data/lib/files.com/models/partner.rb +10 -8
  79. data/lib/files.com/models/partner_channel.rb +10 -8
  80. data/lib/files.com/models/partner_channel_template.rb +10 -8
  81. data/lib/files.com/models/partner_site_request.rb +4 -4
  82. data/lib/files.com/models/payment.rb +3 -3
  83. data/lib/files.com/models/pending_work_event.rb +3 -3
  84. data/lib/files.com/models/permission.rb +4 -4
  85. data/lib/files.com/models/public_hosting_request_log.rb +2 -2
  86. data/lib/files.com/models/public_key.rb +10 -8
  87. data/lib/files.com/models/remote_bandwidth_snapshot.rb +2 -2
  88. data/lib/files.com/models/remote_mount_backend.rb +10 -8
  89. data/lib/files.com/models/remote_server.rb +13 -11
  90. data/lib/files.com/models/remote_server_credential.rb +10 -8
  91. data/lib/files.com/models/request.rb +6 -6
  92. data/lib/files.com/models/restore.rb +4 -4
  93. data/lib/files.com/models/schedule.rb +10 -8
  94. data/lib/files.com/models/scheduled_export.rb +10 -8
  95. data/lib/files.com/models/scim_log.rb +3 -3
  96. data/lib/files.com/models/secret.rb +10 -8
  97. data/lib/files.com/models/session.rb +2 -2
  98. data/lib/files.com/models/settings_change.rb +2 -2
  99. data/lib/files.com/models/sftp_action_log.rb +2 -2
  100. data/lib/files.com/models/sftp_host_key.rb +10 -8
  101. data/lib/files.com/models/share_group.rb +10 -8
  102. data/lib/files.com/models/siem_http_destination.rb +10 -8
  103. data/lib/files.com/models/siem_http_destination_event.rb +3 -3
  104. data/lib/files.com/models/site.rb +3 -3
  105. data/lib/files.com/models/site_subdomain_redirect.rb +3 -3
  106. data/lib/files.com/models/snapshot.rb +10 -8
  107. data/lib/files.com/models/sso_event.rb +3 -3
  108. data/lib/files.com/models/sso_strategy.rb +3 -3
  109. data/lib/files.com/models/style.rb +6 -5
  110. data/lib/files.com/models/sync.rb +10 -8
  111. data/lib/files.com/models/sync_log.rb +2 -2
  112. data/lib/files.com/models/sync_run.rb +3 -3
  113. data/lib/files.com/models/usage_daily_snapshot.rb +2 -2
  114. data/lib/files.com/models/usage_snapshot.rb +2 -2
  115. data/lib/files.com/models/user.rb +10 -8
  116. data/lib/files.com/models/user_additional_email_recipient.rb +10 -8
  117. data/lib/files.com/models/user_cipher_use.rb +2 -2
  118. data/lib/files.com/models/user_lifecycle_rule.rb +10 -8
  119. data/lib/files.com/models/user_request.rb +5 -5
  120. data/lib/files.com/models/user_security_event.rb +3 -3
  121. data/lib/files.com/models/user_sftp_client_use.rb +2 -2
  122. data/lib/files.com/models/web_dav_action_log.rb +2 -2
  123. data/lib/files.com/models/webhook_test.rb +2 -2
  124. data/lib/files.com/models/workspace.rb +10 -8
  125. data/lib/files.com/response.rb +24 -0
  126. data/lib/files.com/sizable_io.rb +65 -4
  127. data/lib/files.com/version.rb +1 -1
  128. data/lib/files.com.rb +23 -1
  129. data/spec/language_spec.rb +20 -10
  130. data/spec/lib/api_client_spec.rb +173 -18
  131. data/spec/list_spec.rb +203 -187
  132. data/spec/models/file_transfer_spec.rb +346 -0
  133. data/spec/models/persistence_spec.rb +167 -0
  134. data/spec/spec_helper.rb +122 -6
  135. data/spec/support/stubbed_api.rb +14 -0
  136. data/spec/support/transfer_server.rb +60 -0
  137. 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, :open_timeout, :read_timeout, :proxy, :session_id, :workspace_id
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
@@ -9,20 +9,30 @@ RSpec.describe "Language setting" do
9
9
  Files.language = old_language
10
10
  end
11
11
 
12
- let(:client) { Files::ApiClient.new }
12
+ describe "request headers" do
13
+ let(:client) { Files::ApiClient.new }
13
14
 
14
- it "sends the configured language as the Accept-Language header" do
15
- Files.language = "es"
15
+ it "sends the configured language as the Accept-Language header" do
16
+ Files.language = "es"
16
17
 
17
- headers = client.send(:request_headers, "api-key", nil, :get, nil)
18
+ headers = client.send(:request_headers, "api-key", nil, :get, nil)
18
19
 
19
- expect(Files.language).to eq("es")
20
- expect(headers["Accept-Language"]).to eq("es")
21
- end
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
- it "omits the Accept-Language header when unset" do
24
- headers = client.send(:request_headers, "api-key", nil, :get, nil)
27
+ expect(headers).not_to have_key("Accept-Language")
28
+ end
29
+ end
25
30
 
26
- expect(headers).not_to have_key("Accept-Language")
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
@@ -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
- Files.logger = Logger.new(output, level: Logger::INFO)
12
- connection = Faraday.new do |builder|
13
- builder.response :raise_error
14
- builder.adapter :test do |stub|
15
- stub.put(url) { [ 404, {}, { type: "not-found", error: url }.to_json ] }
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
- expect { described_class.new(connection).remote_request(:put, url, {}, "part") }.to raise_error(Files::NotFoundError) { |error|
20
- expect(error.http_status).to eq(404)
21
- expect(error.full_message).not_to include(url, "private-part", "secret")
22
- }
23
- expect(output.string).not_to include(url, "private-part", "secret")
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 general api connection error' do
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(Files::APIConnectionError, error_message)
150
+ }.to raise_error(error_class, error_message)
82
151
  end
83
152
  end
84
153
 
85
- context 'when response is a valid json' do
154
+ context 'when response is a Files.com error object' do
86
155
  it_behaves_like 'a server error handler' do
87
- let(:error_message) { "Could not connect to Files.com at URL 1. Please check your internet connection and try again. If this problem persists, you should check Files.com's service status at https://status.files.com, or contact your primary account representative. Request was retried 3 times.\n\n(Network error: {\"error\":\"Server Error\"})" }
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 bad request error when no type" do
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::APIConnectionError)
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