files.com 1.1.795 → 1.1.797
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/_VERSION +1 -1
- data/docs/gpg_key.md +1 -1
- data/lib/files.com/models/file.rb +42 -10
- data/lib/files.com/models/gpg_key.rb +1 -1
- data/spec/api_scenarios_spec.rb +1298 -0
- data/spec/models/file_transfer_spec.rb +191 -0
- data/spec/models/file_underscore_destinations_spec.rb +1 -1
- data/spec/spec_helper.rb +118 -8
- metadata +2 -1
|
@@ -0,0 +1,1298 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "digest"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "find"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "time"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
|
|
9
|
+
# Records, in record, what the SDK's own connection sends and receives for an upload: the parts each successful
|
|
10
|
+
# begin_upload response grants (record[:grants]: each grant's ref, part_number and partsize, by the key of its upload
|
|
11
|
+
# URL), and each part the SDK sends outside the API and receives a successful response for (record[:parts], in the order
|
|
12
|
+
# sent: the key of its URL, and the size and SHA-256 of the body it carried, taken as the request is sent).
|
|
13
|
+
class ApiScenarioPartObserver < Faraday::Middleware
|
|
14
|
+
def initialize(app, record)
|
|
15
|
+
super(app)
|
|
16
|
+
@record = record
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A URL as a key that does not depend on how its query is ordered or escaped.
|
|
20
|
+
def self.key(url)
|
|
21
|
+
uri = URI(url.to_s)
|
|
22
|
+
[ uri.host, uri.port, uri.path, URI.decode_www_form(uri.query.to_s).sort ]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(env)
|
|
26
|
+
body = env.request_body.to_s
|
|
27
|
+
@app.call(env).on_complete do |response|
|
|
28
|
+
next unless response.status.to_i.between?(200, 299)
|
|
29
|
+
|
|
30
|
+
if response.url.path.start_with?("/api/rest/v1/file_actions/begin_upload/")
|
|
31
|
+
JSON.parse(response.response_body.to_s).each { |grant| @record[:grants][self.class.key(grant.fetch("upload_uri"))] = grant.slice("ref", "part_number", "partsize") }
|
|
32
|
+
elsif !response.url.path.start_with?("/api/rest/v1/") && response.method != :get
|
|
33
|
+
@record[:parts] << { key: self.class.key(response.url), bytes: body.bytesize, sha256: Digest::SHA256.hexdigest(body) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A forward-only upload source: it gives its data at most max_read bytes a read, and nil at its end. It can only be
|
|
40
|
+
# read forward; it has no size, position, seek or rewind.
|
|
41
|
+
class ApiScenarioShortReader
|
|
42
|
+
def initialize(data, max_read)
|
|
43
|
+
@data = data
|
|
44
|
+
@max_read = max_read
|
|
45
|
+
@offset = 0
|
|
46
|
+
@closed = false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def read(length = nil, outbuf = nil)
|
|
50
|
+
raise IOError, "closed stream" if @closed
|
|
51
|
+
return nil if eof? && length.to_i.positive?
|
|
52
|
+
|
|
53
|
+
chunk = @data.byteslice(@offset, [ @max_read, length || @max_read ].min)
|
|
54
|
+
@offset += chunk.bytesize
|
|
55
|
+
outbuf ? outbuf.replace(chunk) : chunk
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def eof?
|
|
59
|
+
@offset >= @data.bytesize
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def close
|
|
63
|
+
@closed = true
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def closed?
|
|
68
|
+
@closed
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Generated from verification/scenarios/api.json (SHA-256 5d9b8eff0ef0724a68a458db77dc85784bb7d24436f104b98c2a2fe67086bbe0), the Files.com
|
|
73
|
+
# API scenarios every client shares. Each example runs one scenario's API operations and client actions through
|
|
74
|
+
# this SDK against the mock server's simulator, in a folder of its own beneath the example's test folder. Steps only
|
|
75
|
+
# a Files.com site can answer are left out. The steps this binding admits it does not carry out yet, and each later
|
|
76
|
+
# step that needs what one would have made, are left out and reported by pending examples naming them and why, as is
|
|
77
|
+
# each expectation the SDK gives no way to observe.
|
|
78
|
+
RSpec.describe "Files.com API scenarios" do
|
|
79
|
+
# The block's result and the response its last API request received, through the client's own request
|
|
80
|
+
# capture.
|
|
81
|
+
def with_response(&block)
|
|
82
|
+
Files::ApiClient.active_client.request(&block)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Calls the block up to attempts times, interval_seconds apart, until it raises error_class, and returns
|
|
86
|
+
# that error, or nil if it never did. Any other error ends the example.
|
|
87
|
+
def eventually_raised(error_class, attempts, interval_seconds)
|
|
88
|
+
attempts.times do |attempt|
|
|
89
|
+
sleep interval_seconds if attempt.positive?
|
|
90
|
+
begin
|
|
91
|
+
yield
|
|
92
|
+
rescue error_class => e
|
|
93
|
+
return e
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Each page of the list the block returns, with the HTTP status its request received, following next_page
|
|
100
|
+
# until a page has no next page.
|
|
101
|
+
def pages_of(max_pages)
|
|
102
|
+
list = yield
|
|
103
|
+
records, response = with_response { list.to_a }
|
|
104
|
+
pages = [ [ records, response.http_status ] ]
|
|
105
|
+
while list.has_next_page?
|
|
106
|
+
raise "The list had more than #{max_pages} pages." if pages.size == max_pages
|
|
107
|
+
|
|
108
|
+
list, response = with_response { list.next_page }
|
|
109
|
+
pages << [ list.to_a, response.http_status ]
|
|
110
|
+
end
|
|
111
|
+
pages
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The highest seq in the simulator's journal.
|
|
115
|
+
def journal_seq
|
|
116
|
+
MockServerFixture.journal(Files.base_url).map { |entry| entry.fetch("seq") }.max || 0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Checks a listing's page requests in the simulator's journal (the folders.list requests after seq before): one
|
|
120
|
+
# for each page the SDK returned, the first sending no cursor, each later one sending exactly the cursor the page
|
|
121
|
+
# before it returned, each answered with status, and the last returning no cursor. The journal holds only the
|
|
122
|
+
# cursors' SHA-256s.
|
|
123
|
+
def cursor_chain(before, page_count, status)
|
|
124
|
+
requests = MockServerFixture.journal(Files.base_url).select { |entry| entry.fetch("seq") > before && entry["operation"] == "folders.list" }
|
|
125
|
+
expect(requests.size).to eq(page_count), "The simulator journaled #{requests.size} page requests, not the #{page_count} pages the SDK returned."
|
|
126
|
+
requests.each_with_index do |request, index|
|
|
127
|
+
sent = index.zero? ? nil : requests[index - 1]["next_cursor_sha256"]
|
|
128
|
+
expect([ request["cursor_sha256"], request["status"] ]).to eq([ sent, status ]), "Page #{index + 1} (journal seq #{request["seq"]}) sent a cursor other than the one the page before it returned, or was not answered with #{status}."
|
|
129
|
+
end
|
|
130
|
+
expect(requests.last["next_cursor_sha256"]).to be_nil, "The last page (journal seq #{requests.last["seq"]}) returned a next cursor."
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# A recipe's bytes: byte index is (index * multiplier + offset) % modulus, for each index below length. They
|
|
134
|
+
# must have the length and SHA-256 the definition records.
|
|
135
|
+
def recipe(number, multiplier, offset, modulus, length, sha256)
|
|
136
|
+
(@recipes ||= {})[number] ||= Array.new(length) { |index| ((index * multiplier) + offset) % modulus }.pack("C*").tap do |bytes|
|
|
137
|
+
raise "Recipe #{number} makes #{bytes.bytesize} bytes with SHA-256 #{Digest::SHA256.hexdigest(bytes)}, not #{length} bytes with SHA-256 #{sha256}." unless bytes.bytesize == length && Digest::SHA256.hexdigest(bytes) == sha256
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Checks bytes the client received against the recipe's: their length, their SHA-256 and every byte.
|
|
142
|
+
def same_bytes(what, got, data)
|
|
143
|
+
expect(got == data).to be(true), "#{what} holds #{got.bytesize} bytes with SHA-256 #{Digest::SHA256.hexdigest(got)}, not the #{data.bytesize} bytes with SHA-256 #{Digest::SHA256.hexdigest(data)} expected."
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# A new path in the example's local folder.
|
|
147
|
+
def local_path(name)
|
|
148
|
+
File.join(@scratch, "#{@local_files = (@local_files || 0) + 1}-#{name}")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The entries the scenario's listings returned, by path.
|
|
152
|
+
def listed
|
|
153
|
+
@listed ||= {}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Runs the block with the scenario's options and a client on the SDK's own default connection, which
|
|
157
|
+
# ApiScenarioPartObserver watches into record. The connection is closed afterwards.
|
|
158
|
+
def observing(record)
|
|
159
|
+
client = Files::ApiClient.new(Files::ApiClient.build_default_conn.tap { |connection| connection.builder.insert(0, ApiScenarioPartObserver, record) })
|
|
160
|
+
yield options.merge(client: client)
|
|
161
|
+
ensure
|
|
162
|
+
client&.conn&.close
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Runs the upload the block makes with the options it is given, and checks that it returned the file at exactly
|
|
166
|
+
# path, its size data's as an Integer. With min_parts, or with witness, the upload goes through a client that
|
|
167
|
+
# ApiScenarioPartObserver watches (observing); with min_parts, the SDK must have received a successful response to at
|
|
168
|
+
# least that many parts it sent to upload URLs. Returns what the observer recorded.
|
|
169
|
+
def uploaded(path, data, min_parts, witness: false, &upload)
|
|
170
|
+
record = { grants: {}, parts: [] }
|
|
171
|
+
file = min_parts || witness ? observing(record, &upload) : upload.call(options)
|
|
172
|
+
expect(file).to be_a(Files::File)
|
|
173
|
+
expect(file.path).to eq(path)
|
|
174
|
+
expect(file.size).to be_an(Integer).and eq(data.bytesize)
|
|
175
|
+
sent = record[:parts].map { |part| part[:key] }.uniq.size
|
|
176
|
+
expect(sent).to be >= min_parts, "The SDK received successful responses to #{sent} upload parts, fewer than #{min_parts}." if min_parts
|
|
177
|
+
record
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The grant each part ApiScenarioPartObserver recorded was sent to, bound by its URL.
|
|
181
|
+
def grants_of(record)
|
|
182
|
+
record[:parts].map { |part| record[:grants].fetch(part[:key]) { raise "A part was sent to a URL no begin_upload response granted." } }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Checks the parts an upload of data sent, as ApiScenarioPartObserver recorded them, against the grants they were
|
|
186
|
+
# sent to: all of one ref, numbered 1, 2, ... in the order sent; each but the last carrying exactly its grant's
|
|
187
|
+
# partsize, and the last the rest of data, at most its grant's; their bodies, in order, exactly data. The simulator's
|
|
188
|
+
# journal after seq before must show the same parts (number, bytes and SHA-256), each accepted.
|
|
189
|
+
def filled_parts(record, data, before)
|
|
190
|
+
grants = grants_of(record)
|
|
191
|
+
expect(grants.map { |grant| grant["ref"] }.uniq.size).to eq(1), "The parts were sent under #{grants.map { |grant| grant["ref"] }.uniq.size} refs, not one."
|
|
192
|
+
expect(grants.map { |grant| grant["part_number"] }).to eq((1..grants.size).to_a)
|
|
193
|
+
offset = 0
|
|
194
|
+
record[:parts].each_with_index do |part, index|
|
|
195
|
+
partsize = grants[index]["partsize"]
|
|
196
|
+
want = index < record[:parts].size - 1 ? partsize : [ data.bytesize - offset, partsize ].min
|
|
197
|
+
expect(part[:bytes]).to eq(want), "Part #{index + 1} carried #{part[:bytes]} bytes, not #{want} (its grant's partsize is #{partsize})."
|
|
198
|
+
expect(part[:sha256]).to eq(Digest::SHA256.hexdigest(data.byteslice(offset, part[:bytes]))), "Part #{index + 1} did not carry the next #{part[:bytes]} bytes of the data."
|
|
199
|
+
offset += part[:bytes]
|
|
200
|
+
end
|
|
201
|
+
expect(offset).to eq(data.bytesize)
|
|
202
|
+
journaled = MockServerFixture.journal(Files.base_url).select { |entry| entry.fetch("seq") > before && entry["operation"] == "transfers.upload_part" }
|
|
203
|
+
expect(journaled.map { |entry| [ entry["part"], entry["bytes"], entry["sha256"] ] }).to eq(record[:parts].each_with_index.map { |part, index| [ index + 1, part[:bytes], part[:sha256] ] })
|
|
204
|
+
expect(journaled.map { |entry| entry["status"] }).to all(be_between(200, 299))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Checks the progress reports of an upload of data against the parts ApiScenarioPartObserver saw acknowledged: one
|
|
208
|
+
# report for each, in order, with its grant's part number and its byte count; acknowledged bytes adding up; and every
|
|
209
|
+
# report giving data's length as its total, so that the last gives it as both its total and its acknowledged bytes.
|
|
210
|
+
def final_progress(reports, record, data)
|
|
211
|
+
sizes = record[:parts].map { |part| part[:bytes] }
|
|
212
|
+
expect(reports.map { |report| [ report[:part_number], report[:part_bytes] ] }).to eq(grants_of(record).map { |grant| grant["part_number"] }.zip(sizes))
|
|
213
|
+
expect(reports.map { |report| report[:acknowledged_bytes] }).to eq(sizes.each_index.map { |index| sizes.take(index + 1).sum })
|
|
214
|
+
expect(reports.map { |report| report[:total_bytes] }).to all(eq(data.bytesize))
|
|
215
|
+
expect([ reports.last[:total_bytes], reports.last[:acknowledged_bytes] ]).to eq([ data.bytesize, data.bytesize ])
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# A new local file holding data, with provided_mtime as its modification time, which Files::File.upload_file sends as
|
|
219
|
+
# the upload's provided mtime.
|
|
220
|
+
def local_file(path, data, provided_mtime)
|
|
221
|
+
local = local_path(File.basename(path))
|
|
222
|
+
File.binwrite(local, data)
|
|
223
|
+
File.utime(Time.iso8601(provided_mtime), Time.iso8601(provided_mtime), local) if provided_mtime
|
|
224
|
+
local
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Uploads data from a new local file to path with Files::File.upload_file. With progress_final, the upload's
|
|
228
|
+
# progress callable records each report, and the reports must end at data's length (final_progress).
|
|
229
|
+
def upload_local_file(path, data, provided_mtime: nil, min_parts: nil, progress_final: false)
|
|
230
|
+
local = local_file(path, data, provided_mtime)
|
|
231
|
+
reports = []
|
|
232
|
+
record = uploaded(path, data, min_parts, witness: progress_final) do |upload_options|
|
|
233
|
+
next Files::File.upload_file(local, path, upload_options) unless progress_final
|
|
234
|
+
|
|
235
|
+
Files::File.upload_file(local, path, upload_options, progress: ->(report) { reports << report })
|
|
236
|
+
end
|
|
237
|
+
final_progress(reports, record, data) if progress_final
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Uploads data from a new local file to path with Files::File.upload_file and a progress callable that cancels the
|
|
241
|
+
# upload by raising at its first report, once the first part is acknowledged. That same exception must come back after
|
|
242
|
+
# that one report, of the first part ApiScenarioPartObserver saw acknowledged, filled to its grant's partsize (or all
|
|
243
|
+
# of data). The simulator's journal must show that part accepted, and no planning request, part or finalize after it;
|
|
244
|
+
# parts granted before it may stay unsent.
|
|
245
|
+
def upload_cancelled(path, data, provided_mtime: nil)
|
|
246
|
+
local = local_file(path, data, provided_mtime)
|
|
247
|
+
cancel = StandardError.new("The scenario cancelled the upload at its first acknowledged part.")
|
|
248
|
+
reports = []
|
|
249
|
+
cancel_at_first_report = lambda do |report|
|
|
250
|
+
reports << report
|
|
251
|
+
raise cancel
|
|
252
|
+
end
|
|
253
|
+
record = { grants: {}, parts: [] }
|
|
254
|
+
before = journal_seq
|
|
255
|
+
expect { observing(record) { |upload_options| Files::File.upload_file(local, path, upload_options, progress: cancel_at_first_report) } }
|
|
256
|
+
.to raise_error(StandardError) { |error| expect(error).to equal(cancel) }
|
|
257
|
+
|
|
258
|
+
expect(record[:parts].size).to eq(1), "The SDK sent #{record[:parts].size} parts that were acknowledged, not one."
|
|
259
|
+
grant = grants_of(record).first
|
|
260
|
+
part = record[:parts].first
|
|
261
|
+
expect(part[:bytes]).to eq([ grant["partsize"], data.bytesize ].min)
|
|
262
|
+
expect(part[:sha256]).to eq(Digest::SHA256.hexdigest(data.byteslice(0, part[:bytes])))
|
|
263
|
+
expect(reports).to eq([ { total_bytes: data.bytesize, acknowledged_bytes: part[:bytes], part_number: grant["part_number"], part_bytes: part[:bytes] } ])
|
|
264
|
+
journal = MockServerFixture.journal(Files.base_url).select { |entry| entry.fetch("seq") > before && %w[files.begin_upload transfers.upload_part files.finalize_upload].include?(entry["operation"]) }
|
|
265
|
+
sent = journal.index { |entry| entry["operation"] == "transfers.upload_part" }
|
|
266
|
+
expect(sent).not_to be_nil, "The simulator journaled no part of the cancelled upload."
|
|
267
|
+
expect(journal[sent]["status"]).to be_between(200, 299)
|
|
268
|
+
expect(journal.drop(sent + 1).map { |entry| entry["operation"] }).to be_empty, "The cancelled upload went on after its first acknowledged part."
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Uploads data to path from a forward-only source giving at most max_read bytes a read (ApiScenarioShortReader):
|
|
272
|
+
# Files::File#write takes it, and #close uploads it, sending the provided mtime it is given, and closes it. The
|
|
273
|
+
# upload is watched, and its parts must be filled to the sizes their grants gave (filled_parts).
|
|
274
|
+
def upload_forward_only(path, data, max_read:, provided_mtime: nil, min_parts: nil)
|
|
275
|
+
reader = ApiScenarioShortReader.new(data, max_read)
|
|
276
|
+
before = journal_seq
|
|
277
|
+
record = uploaded(path, data, min_parts, witness: true) do |upload_options|
|
|
278
|
+
file = Files::File.new(path, "w", upload_options)
|
|
279
|
+
file.write(reader)
|
|
280
|
+
provided_mtime ? file.close(provided_mtime: provided_mtime) : file.close
|
|
281
|
+
file
|
|
282
|
+
end
|
|
283
|
+
expect(reader).to be_closed
|
|
284
|
+
filled_parts(record, data, before)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Uploads data to path from a seekable stream (a StringIO): Files::File#write takes the stream, and #close uploads
|
|
288
|
+
# it, sending the provided mtime it is given, and closes it. The stream is the SDK's once written.
|
|
289
|
+
def upload_stream(path, data, provided_mtime: nil, min_parts: nil)
|
|
290
|
+
stream = StringIO.new(data.dup)
|
|
291
|
+
uploaded(path, data, min_parts) do |upload_options|
|
|
292
|
+
file = Files::File.new(path, "w", upload_options)
|
|
293
|
+
file.write(stream)
|
|
294
|
+
provided_mtime ? file.close(provided_mtime: provided_mtime) : file.close
|
|
295
|
+
file
|
|
296
|
+
end
|
|
297
|
+
expect(stream).to be_closed
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Downloads the file at path with #download_file into a new local file, from the file Files::File.download
|
|
301
|
+
# returns for path, or from the entry a listing returned for it, and checks its bytes.
|
|
302
|
+
def download(path, data, listed_entry: false)
|
|
303
|
+
file = listed_entry ? listed.fetch(path) { raise "No listing returned an entry for #{path}." } : Files::File.download(path, {}, options)
|
|
304
|
+
local = local_path(File.basename(path))
|
|
305
|
+
file.download_file(local)
|
|
306
|
+
same_bytes("The download of #{path}", File.binread(local), data)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# The status a copy or move ends with: its FileAction's own when that is completed or failed, or else its
|
|
310
|
+
# migration's, looked up with the block (given the migration's id) at once and then every second until it is
|
|
311
|
+
# completed or failed, for at most a minute.
|
|
312
|
+
def migrated(action)
|
|
313
|
+
return action.status if %w[completed failed].include?(action.status)
|
|
314
|
+
|
|
315
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 60
|
|
316
|
+
loop do
|
|
317
|
+
status = yield(action.file_migration_id).status
|
|
318
|
+
return status if %w[completed failed].include?(status)
|
|
319
|
+
raise "The migration #{action.file_migration_id} was still #{status.inspect} after a minute." if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
320
|
+
|
|
321
|
+
sleep 1
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Every file and folder inside the local folder root, at any depth, as paths relative to it, those whose names begin
|
|
326
|
+
# with a dot included. Find.find never yields a folder's "." or ".." entries; only root itself is left out.
|
|
327
|
+
def local_entries(root)
|
|
328
|
+
entries = []
|
|
329
|
+
Find.find(root) { |path| entries << path.delete_prefix("#{root}/") unless path == root }
|
|
330
|
+
entries
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Uploads a local tree to remote with this SDK's own calls: makes the tree (paths relative to it, each with its
|
|
334
|
+
# recipe's bytes for a file and nil for a folder) in a new local folder, walks that folder, creates remote and then
|
|
335
|
+
# each folder found there, parents first, with the block (the folder create operation), and uploads each file found
|
|
336
|
+
# with Files::File.upload_file. What the API returned must be what the definition expects: the files the uploads
|
|
337
|
+
# returned exactly completed (relative paths), files of them, each at its path with its local size as an Integer,
|
|
338
|
+
# bytes in all; the folders the creates returned exactly directories; and those holding nothing uploaded or created
|
|
339
|
+
# exactly empty_directories, empty_folders of them.
|
|
340
|
+
def upload_directory(remote, tree, completed:, files:, bytes:, directories:, empty_directories:, empty_folders:)
|
|
341
|
+
local = local_path("upload-directory")
|
|
342
|
+
Dir.mkdir(local)
|
|
343
|
+
tree.each do |relative, data|
|
|
344
|
+
target = File.join(local, relative)
|
|
345
|
+
FileUtils.mkdir_p(data ? File.dirname(target) : target)
|
|
346
|
+
File.binwrite(target, data) if data
|
|
347
|
+
end
|
|
348
|
+
found = local_entries(local)
|
|
349
|
+
folders = found.select { |relative| File.directory?(File.join(local, relative)) }.sort_by { |relative| [ relative.count("/"), relative ] }
|
|
350
|
+
|
|
351
|
+
root = yield(remote)
|
|
352
|
+
expect([ root.path, root.type ]).to eq([ remote, "directory" ])
|
|
353
|
+
created = folders.map do |relative|
|
|
354
|
+
folder = yield("#{remote}/#{relative}")
|
|
355
|
+
expect(folder.type).to eq("directory")
|
|
356
|
+
expect(folder.path).to start_with("#{remote}/")
|
|
357
|
+
folder.path.delete_prefix("#{remote}/")
|
|
358
|
+
end
|
|
359
|
+
done = (found - folders).sort.map do |relative|
|
|
360
|
+
file = Files::File.upload_file(File.join(local, relative), "#{remote}/#{relative}", options)
|
|
361
|
+
expect(file.path).to eq("#{remote}/#{relative}")
|
|
362
|
+
expect(file.size).to be_an(Integer).and eq(File.size(File.join(local, relative)))
|
|
363
|
+
[ relative, file.size ]
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
expect(done.map(&:first)).to contain_exactly(*completed)
|
|
367
|
+
expect(done.size).to eq(files)
|
|
368
|
+
expect(done.sum(&:last)).to eq(bytes)
|
|
369
|
+
expect(created).to contain_exactly(*directories)
|
|
370
|
+
empty = created.reject { |folder| (done.map(&:first) + created).any? { |path| path.start_with?("#{folder}/") } }
|
|
371
|
+
expect(empty).to contain_exactly(*empty_directories)
|
|
372
|
+
expect(empty.size).to eq(empty_folders)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Downloads the folder at remote into a new local folder with this SDK's own calls: lists each folder, every page,
|
|
376
|
+
# with the block (the folder list operation), makes each folder listed in it locally and downloads each file listed
|
|
377
|
+
# with #download_file. Then walks the local folder: it must hold files files, bytes bytes in all, and empty_folders
|
|
378
|
+
# empty folders, and each of matched (a path relative to it and a recipe's bytes) exactly those bytes.
|
|
379
|
+
def download_directory(remote, matched, files:, empty_folders:, bytes:)
|
|
380
|
+
local = local_path("download-directory")
|
|
381
|
+
Dir.mkdir(local)
|
|
382
|
+
folders = [ [ remote, local ] ]
|
|
383
|
+
until folders.empty?
|
|
384
|
+
folder, target = folders.shift
|
|
385
|
+
yield(folder).auto_paging_each do |entry|
|
|
386
|
+
name = File.basename(entry.path)
|
|
387
|
+
expect(entry.path).to eq("#{folder}/#{name}")
|
|
388
|
+
destination = File.join(target, name)
|
|
389
|
+
if entry.type == "directory"
|
|
390
|
+
Dir.mkdir(destination)
|
|
391
|
+
folders << [ entry.path, destination ]
|
|
392
|
+
else
|
|
393
|
+
entry.download_file(destination)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
found = local_entries(local).map { |relative| File.join(local, relative) }
|
|
399
|
+
local_files = found.select { |path| File.file?(path) }
|
|
400
|
+
empty = found.select { |path| File.directory?(path) && Dir.empty?(path) }
|
|
401
|
+
got = { files: local_files.size, bytes: local_files.sum { |path| File.size(path) }, empty_folders: empty.size }
|
|
402
|
+
expect(got).to eq({ files: files, bytes: bytes, empty_folders: empty_folders })
|
|
403
|
+
matched.each { |relative, data| same_bytes("The downloaded #{relative}", File.binread(File.join(local, relative)), data) }
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
context "with a test folder on the simulator", :with_test_folder, api_scenarios: "5d9b8eff0ef0724a68a458db77dc85784bb7d24436f104b98c2a2fe67086bbe0" do
|
|
407
|
+
let(:namespace) { test_folder.join("scenario") }
|
|
408
|
+
|
|
409
|
+
around do |example|
|
|
410
|
+
Dir.mktmpdir("files-api-scenarios") do |scratch|
|
|
411
|
+
@scratch = scratch
|
|
412
|
+
example.run
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
it "files.owned-folder-namespace: Folders made one at a time inside the owned folder: each found by path, an existing name refused, the owned folder listed one entry per page with exactly its own children, and every folder deleted and confirmed absent. API operations only." do
|
|
417
|
+
# Step 1: FileActionFind (GET /file_actions/metadata/{path}), 422 bad-request/request-params-required
|
|
418
|
+
expect { Files::File.find("/", {}, options) }.to raise_error(Files::RequestParamsRequiredError) { |error|
|
|
419
|
+
expect(error.http_status).to eq(422)
|
|
420
|
+
expect(error.type).to eq("bad-request/request-params-required")
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
# Step 2: FolderListForPath (GET /folders/{path}), 200
|
|
424
|
+
before = journal_seq
|
|
425
|
+
pages = pages_of(16) { Files::Folder.list_for("/", { per_page: 1000 }, options) }
|
|
426
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
427
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
428
|
+
cursor_chain(before, pages.size, 200)
|
|
429
|
+
records = pages.flat_map(&:first)
|
|
430
|
+
expect(records).to all(be_a(Files::File))
|
|
431
|
+
# The listing's members (the root's) are the site's: its pages are checked, not its members.
|
|
432
|
+
|
|
433
|
+
# Step 3 (BehaviorListForPath) is for a Files.com site only.
|
|
434
|
+
|
|
435
|
+
# Step 4: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
436
|
+
expect { Files::File.find(namespace.to_s, {}, options) }.to raise_error(Files::NotFoundError) { |error|
|
|
437
|
+
expect(error.http_status).to eq(404)
|
|
438
|
+
expect(error.type).to eq("not-found")
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# Step 5: PostFoldersPath (POST /folders/{path}), 201
|
|
442
|
+
entry, response = with_response { Files::Folder.create(namespace.to_s, { mkdir_parents: false }, options) }
|
|
443
|
+
expect(response.http_status).to eq(201)
|
|
444
|
+
expect(entry).to be_a(Files::File)
|
|
445
|
+
expect(entry.type).to eq("directory")
|
|
446
|
+
expect(entry.path).to eq(namespace.to_s)
|
|
447
|
+
expect(entry.size).to be_nil
|
|
448
|
+
|
|
449
|
+
# Step 6 (FileActionFind) is for a Files.com site only.
|
|
450
|
+
|
|
451
|
+
# Step 7: PostFoldersPath (POST /folders/{path}), 201
|
|
452
|
+
entry, response = with_response { Files::Folder.create(namespace.join("alpha").to_s, { mkdir_parents: false }, options) }
|
|
453
|
+
expect(response.http_status).to eq(201)
|
|
454
|
+
expect(entry).to be_a(Files::File)
|
|
455
|
+
expect(entry.type).to eq("directory")
|
|
456
|
+
expect(entry.path).to eq(namespace.join("alpha").to_s)
|
|
457
|
+
expect(entry.size).to be_nil
|
|
458
|
+
|
|
459
|
+
# Step 8: PostFoldersPath (POST /folders/{path}), 201
|
|
460
|
+
entry, response = with_response { Files::Folder.create(namespace.join("bravo").to_s, { mkdir_parents: false }, options) }
|
|
461
|
+
expect(response.http_status).to eq(201)
|
|
462
|
+
expect(entry).to be_a(Files::File)
|
|
463
|
+
expect(entry.type).to eq("directory")
|
|
464
|
+
expect(entry.path).to eq(namespace.join("bravo").to_s)
|
|
465
|
+
expect(entry.size).to be_nil
|
|
466
|
+
|
|
467
|
+
# Step 9: PostFoldersPath (POST /folders/{path}), 201
|
|
468
|
+
entry, response = with_response { Files::Folder.create(namespace.join("charlie").to_s, { mkdir_parents: false }, options) }
|
|
469
|
+
expect(response.http_status).to eq(201)
|
|
470
|
+
expect(entry).to be_a(Files::File)
|
|
471
|
+
expect(entry.type).to eq("directory")
|
|
472
|
+
expect(entry.path).to eq(namespace.join("charlie").to_s)
|
|
473
|
+
expect(entry.size).to be_nil
|
|
474
|
+
|
|
475
|
+
# Step 10: PostFoldersPath (POST /folders/{path}), 201
|
|
476
|
+
entry, response = with_response { Files::Folder.create(namespace.join("alpha/inner").to_s, { mkdir_parents: false }, options) }
|
|
477
|
+
expect(response.http_status).to eq(201)
|
|
478
|
+
expect(entry).to be_a(Files::File)
|
|
479
|
+
expect(entry.type).to eq("directory")
|
|
480
|
+
expect(entry.path).to eq(namespace.join("alpha/inner").to_s)
|
|
481
|
+
expect(entry.size).to be_nil
|
|
482
|
+
|
|
483
|
+
# Step 11: PostFoldersPath (POST /folders/{path}), 422 processing-failure/destination-exists
|
|
484
|
+
expect { Files::Folder.create(namespace.join("alpha").to_s, { mkdir_parents: false }, options) }.to raise_error(Files::DestinationExistsError) { |error|
|
|
485
|
+
expect(error.http_status).to eq(422)
|
|
486
|
+
expect(error.type).to eq("processing-failure/destination-exists")
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
# Step 12: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
490
|
+
entry, response = with_response { Files::File.find(namespace.join("alpha").to_s, {}, options) }
|
|
491
|
+
expect(response.http_status).to eq(200)
|
|
492
|
+
expect(entry).to be_a(Files::File)
|
|
493
|
+
expect(entry.type).to eq("directory")
|
|
494
|
+
expect(entry.path).to eq(namespace.join("alpha").to_s)
|
|
495
|
+
expect(entry.size).to be_nil
|
|
496
|
+
|
|
497
|
+
# Step 13: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
498
|
+
entry, response = with_response { Files::File.find(namespace.join("alpha/inner").to_s, {}, options) }
|
|
499
|
+
expect(response.http_status).to eq(200)
|
|
500
|
+
expect(entry).to be_a(Files::File)
|
|
501
|
+
expect(entry.type).to eq("directory")
|
|
502
|
+
expect(entry.path).to eq(namespace.join("alpha/inner").to_s)
|
|
503
|
+
expect(entry.size).to be_nil
|
|
504
|
+
|
|
505
|
+
# Step 14: FolderListForPath (GET /folders/{path}), 200
|
|
506
|
+
before = journal_seq
|
|
507
|
+
pages = pages_of(16) { Files::Folder.list_for(namespace.to_s, { per_page: 1 }, options) }
|
|
508
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
509
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1)
|
|
510
|
+
cursor_chain(before, pages.size, 200)
|
|
511
|
+
records = pages.flat_map(&:first)
|
|
512
|
+
expect(records).to all(be_a(Files::File))
|
|
513
|
+
expect(records.map { |record| [ record.path, record.type ] }).to contain_exactly(
|
|
514
|
+
[ namespace.join("alpha").to_s, "directory" ],
|
|
515
|
+
[ namespace.join("bravo").to_s, "directory" ],
|
|
516
|
+
[ namespace.join("charlie").to_s, "directory" ],
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Step 15: FolderListForPath (GET /folders/{path}), 200
|
|
520
|
+
before = journal_seq
|
|
521
|
+
pages = pages_of(16) { Files::Folder.list_for(namespace.join("alpha").to_s, { per_page: 1000 }, options) }
|
|
522
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
523
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
524
|
+
cursor_chain(before, pages.size, 200)
|
|
525
|
+
records = pages.flat_map(&:first)
|
|
526
|
+
expect(records).to all(be_a(Files::File))
|
|
527
|
+
expect(records.map { |record| [ record.path, record.type ] }).to contain_exactly(
|
|
528
|
+
[ namespace.join("alpha/inner").to_s, "directory" ],
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Step 16: DeleteFilesPath (DELETE /files/{path}), 204
|
|
532
|
+
result, response = with_response { Files::File.delete(namespace.join("alpha/inner").to_s, { recursive: false }, options) }
|
|
533
|
+
expect(response.http_status).to eq(204)
|
|
534
|
+
expect(result).to be_nil
|
|
535
|
+
|
|
536
|
+
# Step 17: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
537
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("alpha/inner").to_s, {}, options) }
|
|
538
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
539
|
+
expect(error.http_status).to eq(404)
|
|
540
|
+
expect(error.type).to eq("not-found")
|
|
541
|
+
|
|
542
|
+
# Step 18: DeleteFilesPath (DELETE /files/{path}), 204
|
|
543
|
+
result, response = with_response { Files::File.delete(namespace.join("alpha").to_s, { recursive: false }, options) }
|
|
544
|
+
expect(response.http_status).to eq(204)
|
|
545
|
+
expect(result).to be_nil
|
|
546
|
+
|
|
547
|
+
# Step 19: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
548
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("alpha").to_s, {}, options) }
|
|
549
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
550
|
+
expect(error.http_status).to eq(404)
|
|
551
|
+
expect(error.type).to eq("not-found")
|
|
552
|
+
|
|
553
|
+
# Step 20: DeleteFilesPath (DELETE /files/{path}), 204
|
|
554
|
+
result, response = with_response { Files::File.delete(namespace.join("bravo").to_s, { recursive: false }, options) }
|
|
555
|
+
expect(response.http_status).to eq(204)
|
|
556
|
+
expect(result).to be_nil
|
|
557
|
+
|
|
558
|
+
# Step 21: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
559
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("bravo").to_s, {}, options) }
|
|
560
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
561
|
+
expect(error.http_status).to eq(404)
|
|
562
|
+
expect(error.type).to eq("not-found")
|
|
563
|
+
|
|
564
|
+
# Step 22: DeleteFilesPath (DELETE /files/{path}), 204
|
|
565
|
+
result, response = with_response { Files::File.delete(namespace.join("charlie").to_s, { recursive: false }, options) }
|
|
566
|
+
expect(response.http_status).to eq(204)
|
|
567
|
+
expect(result).to be_nil
|
|
568
|
+
|
|
569
|
+
# Step 23: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
570
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("charlie").to_s, {}, options) }
|
|
571
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
572
|
+
expect(error.http_status).to eq(404)
|
|
573
|
+
expect(error.type).to eq("not-found")
|
|
574
|
+
|
|
575
|
+
# Step 24: DeleteFilesPath (DELETE /files/{path}), 204
|
|
576
|
+
result, response = with_response { Files::File.delete(namespace.to_s, { recursive: false }, options) }
|
|
577
|
+
expect(response.http_status).to eq(204)
|
|
578
|
+
expect(result).to be_nil
|
|
579
|
+
|
|
580
|
+
# Step 25: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
581
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.to_s, {}, options) }
|
|
582
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
583
|
+
expect(error.http_status).to eq(404)
|
|
584
|
+
expect(error.type).to eq("not-found")
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
it "files.owned-binary-roundtrip: Three owned files (from a local file, a seekable stream and a forward-only stream giving at most three bytes per read) uploaded with a provided mtime, each found with its exact size, the owned folder listed one entry per page with exactly those files, each downloaded byte for byte, and everything deleted and confirmed absent. Conserved from verification/files-paired/contract.json revision 3 and its journeys." do
|
|
588
|
+
# Step 1: FileActionFind (GET /file_actions/metadata/{path}), 422 bad-request/request-params-required
|
|
589
|
+
expect { Files::File.find("/", {}, options) }.to raise_error(Files::RequestParamsRequiredError) { |error|
|
|
590
|
+
expect(error.http_status).to eq(422)
|
|
591
|
+
expect(error.type).to eq("bad-request/request-params-required")
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
# Step 2: FolderListForPath (GET /folders/{path}), 200
|
|
595
|
+
before = journal_seq
|
|
596
|
+
pages = pages_of(16) { Files::Folder.list_for("/", { per_page: 1000 }, options) }
|
|
597
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
598
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
599
|
+
cursor_chain(before, pages.size, 200)
|
|
600
|
+
records = pages.flat_map(&:first)
|
|
601
|
+
expect(records).to all(be_a(Files::File))
|
|
602
|
+
# The listing's members (the root's) are the site's: its pages are checked, not its members.
|
|
603
|
+
|
|
604
|
+
# Step 3 (BehaviorListForPath) is for a Files.com site only.
|
|
605
|
+
|
|
606
|
+
# Step 4: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
607
|
+
expect { Files::File.find(namespace.to_s, {}, options) }.to raise_error(Files::NotFoundError) { |error|
|
|
608
|
+
expect(error.http_status).to eq(404)
|
|
609
|
+
expect(error.type).to eq("not-found")
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
# Step 5: PostFoldersPath (POST /folders/{path}), 201
|
|
613
|
+
entry, response = with_response { Files::Folder.create(namespace.to_s, { mkdir_parents: false }, options) }
|
|
614
|
+
expect(response.http_status).to eq(201)
|
|
615
|
+
expect(entry).to be_a(Files::File)
|
|
616
|
+
expect(entry.type).to eq("directory")
|
|
617
|
+
expect(entry.path).to eq(namespace.to_s)
|
|
618
|
+
expect(entry.size).to be_nil
|
|
619
|
+
|
|
620
|
+
# Step 6 (FileActionFind) is for a Files.com site only.
|
|
621
|
+
|
|
622
|
+
# Step 7: upload binary.bin from a local-file
|
|
623
|
+
upload_local_file(namespace.join("binary.bin").to_s, recipe(1, 131, 17, 251, 65539, "b65094d36ac1009dac9c73c27b48638a47eb7f23d219a80d1b4d05b58f514499"), provided_mtime: "2026-09-24T00:00:00Z")
|
|
624
|
+
|
|
625
|
+
# Step 8: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
626
|
+
entry, response = with_response { Files::File.find(namespace.join("binary.bin").to_s, {}, options) }
|
|
627
|
+
expect(response.http_status).to eq(200)
|
|
628
|
+
expect(entry).to be_a(Files::File)
|
|
629
|
+
expect(entry.type).to eq("file")
|
|
630
|
+
expect(entry.path).to eq(namespace.join("binary.bin").to_s)
|
|
631
|
+
expect(entry.size).to be_an(Integer).and eq(65539)
|
|
632
|
+
expect([ Time.iso8601("2026-09-24T00:00:00Z"), nil ]).to include((entry.provided_mtime && Time.iso8601(entry.provided_mtime)))
|
|
633
|
+
|
|
634
|
+
# Step 9: upload empty.bin from a seekable-stream
|
|
635
|
+
upload_stream(namespace.join("empty.bin").to_s, recipe(2, 1, 0, 256, 0, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), provided_mtime: "2026-09-24T00:00:00Z")
|
|
636
|
+
|
|
637
|
+
# Step 10: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
638
|
+
entry, response = with_response { Files::File.find(namespace.join("empty.bin").to_s, {}, options) }
|
|
639
|
+
expect(response.http_status).to eq(200)
|
|
640
|
+
expect(entry).to be_a(Files::File)
|
|
641
|
+
expect(entry.type).to eq("file")
|
|
642
|
+
expect(entry.path).to eq(namespace.join("empty.bin").to_s)
|
|
643
|
+
expect(entry.size).to be_an(Integer).and eq(0)
|
|
644
|
+
expect([ Time.iso8601("2026-09-24T00:00:00Z"), nil ]).to include((entry.provided_mtime && Time.iso8601(entry.provided_mtime)))
|
|
645
|
+
|
|
646
|
+
# Step 11: upload short-read.bin from a forward-only-stream
|
|
647
|
+
upload_forward_only(namespace.join("short-read.bin").to_s, recipe(3, 29, 101, 256, 65537, "ed5a86d5b614d5385a85c170c15f25fe75c3faab6a8b29abbb470a6c51faa7f9"), max_read: 3, provided_mtime: "2026-09-24T00:00:00Z")
|
|
648
|
+
|
|
649
|
+
# Step 12: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
650
|
+
entry, response = with_response { Files::File.find(namespace.join("short-read.bin").to_s, {}, options) }
|
|
651
|
+
expect(response.http_status).to eq(200)
|
|
652
|
+
expect(entry).to be_a(Files::File)
|
|
653
|
+
expect(entry.type).to eq("file")
|
|
654
|
+
expect(entry.path).to eq(namespace.join("short-read.bin").to_s)
|
|
655
|
+
expect(entry.size).to be_an(Integer).and eq(65537)
|
|
656
|
+
expect([ Time.iso8601("2026-09-24T00:00:00Z"), nil ]).to include((entry.provided_mtime && Time.iso8601(entry.provided_mtime)))
|
|
657
|
+
|
|
658
|
+
# Step 13: FolderListForPath (GET /folders/{path}), 200
|
|
659
|
+
before = journal_seq
|
|
660
|
+
pages = pages_of(16) { Files::Folder.list_for(namespace.to_s, { per_page: 1 }, options) }
|
|
661
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
662
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1)
|
|
663
|
+
cursor_chain(before, pages.size, 200)
|
|
664
|
+
records = pages.flat_map(&:first)
|
|
665
|
+
expect(records).to all(be_a(Files::File))
|
|
666
|
+
expect(records.map { |record| [ record.path, record.type ] }).to contain_exactly(
|
|
667
|
+
[ namespace.join("binary.bin").to_s, "file" ],
|
|
668
|
+
[ namespace.join("empty.bin").to_s, "file" ],
|
|
669
|
+
[ namespace.join("short-read.bin").to_s, "file" ],
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# Step 14: download binary.bin
|
|
673
|
+
download(namespace.join("binary.bin").to_s, recipe(1, 131, 17, 251, 65539, "b65094d36ac1009dac9c73c27b48638a47eb7f23d219a80d1b4d05b58f514499"))
|
|
674
|
+
|
|
675
|
+
# Step 15: download empty.bin
|
|
676
|
+
download(namespace.join("empty.bin").to_s, recipe(2, 1, 0, 256, 0, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
|
|
677
|
+
|
|
678
|
+
# Step 16: download short-read.bin
|
|
679
|
+
download(namespace.join("short-read.bin").to_s, recipe(3, 29, 101, 256, 65537, "ed5a86d5b614d5385a85c170c15f25fe75c3faab6a8b29abbb470a6c51faa7f9"))
|
|
680
|
+
|
|
681
|
+
# Step 17: DeleteFilesPath (DELETE /files/{path}), 204
|
|
682
|
+
result, response = with_response { Files::File.delete(namespace.join("binary.bin").to_s, { recursive: false }, options) }
|
|
683
|
+
expect(response.http_status).to eq(204)
|
|
684
|
+
expect(result).to be_nil
|
|
685
|
+
|
|
686
|
+
# Step 18: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
687
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("binary.bin").to_s, {}, options) }
|
|
688
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
689
|
+
expect(error.http_status).to eq(404)
|
|
690
|
+
expect(error.type).to eq("not-found")
|
|
691
|
+
|
|
692
|
+
# Step 19: DeleteFilesPath (DELETE /files/{path}), 204
|
|
693
|
+
result, response = with_response { Files::File.delete(namespace.join("empty.bin").to_s, { recursive: false }, options) }
|
|
694
|
+
expect(response.http_status).to eq(204)
|
|
695
|
+
expect(result).to be_nil
|
|
696
|
+
|
|
697
|
+
# Step 20: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
698
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("empty.bin").to_s, {}, options) }
|
|
699
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
700
|
+
expect(error.http_status).to eq(404)
|
|
701
|
+
expect(error.type).to eq("not-found")
|
|
702
|
+
|
|
703
|
+
# Step 21: DeleteFilesPath (DELETE /files/{path}), 204
|
|
704
|
+
result, response = with_response { Files::File.delete(namespace.join("short-read.bin").to_s, { recursive: false }, options) }
|
|
705
|
+
expect(response.http_status).to eq(204)
|
|
706
|
+
expect(result).to be_nil
|
|
707
|
+
|
|
708
|
+
# Step 22: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
709
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("short-read.bin").to_s, {}, options) }
|
|
710
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
711
|
+
expect(error.http_status).to eq(404)
|
|
712
|
+
expect(error.type).to eq("not-found")
|
|
713
|
+
|
|
714
|
+
# Step 23: DeleteFilesPath (DELETE /files/{path}), 204
|
|
715
|
+
result, response = with_response { Files::File.delete(namespace.to_s, { recursive: false }, options) }
|
|
716
|
+
expect(response.http_status).to eq(204)
|
|
717
|
+
expect(result).to be_nil
|
|
718
|
+
|
|
719
|
+
# Step 24: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
720
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.to_s, {}, options) }
|
|
721
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
722
|
+
expect(error.http_status).to eq(404)
|
|
723
|
+
expect(error.type).to eq("not-found")
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
it "files.owned-namespace-extended: Literal special-character names (#, %, a non-ASCII letter and a space), a nested folder, a multipart upload stored in at least two parts with its final progress, a copy and a move waited for, a one-entry-per-page listing whose returned entries download their own files, byte-exact downloads, an upload cancelled after its first acknowledged part that publishes nothing, and non-recursive deletes confirmed by absence. Conserved from verification/files-paired/contract-extended.json revision 2 and its journeys." do
|
|
727
|
+
# Step 1: FileActionFind (GET /file_actions/metadata/{path}), 422 bad-request/request-params-required
|
|
728
|
+
expect { Files::File.find("/", {}, options) }.to raise_error(Files::RequestParamsRequiredError) { |error|
|
|
729
|
+
expect(error.http_status).to eq(422)
|
|
730
|
+
expect(error.type).to eq("bad-request/request-params-required")
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
# Step 2: FolderListForPath (GET /folders/{path}), 200
|
|
734
|
+
before = journal_seq
|
|
735
|
+
pages = pages_of(16) { Files::Folder.list_for("/", { per_page: 1000 }, options) }
|
|
736
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
737
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
738
|
+
cursor_chain(before, pages.size, 200)
|
|
739
|
+
records = pages.flat_map(&:first)
|
|
740
|
+
expect(records).to all(be_a(Files::File))
|
|
741
|
+
# The listing's members (the root's) are the site's: its pages are checked, not its members.
|
|
742
|
+
|
|
743
|
+
# Step 3 (BehaviorListForPath) is for a Files.com site only.
|
|
744
|
+
|
|
745
|
+
# Step 4: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
746
|
+
expect { Files::File.find(namespace.to_s, {}, options) }.to raise_error(Files::NotFoundError) { |error|
|
|
747
|
+
expect(error.http_status).to eq(404)
|
|
748
|
+
expect(error.type).to eq("not-found")
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
# Step 5: PostFoldersPath (POST /folders/{path}), 201
|
|
752
|
+
entry, response = with_response { Files::Folder.create(namespace.to_s, { mkdir_parents: false }, options) }
|
|
753
|
+
expect(response.http_status).to eq(201)
|
|
754
|
+
expect(entry).to be_a(Files::File)
|
|
755
|
+
expect(entry.type).to eq("directory")
|
|
756
|
+
expect(entry.path).to eq(namespace.to_s)
|
|
757
|
+
expect(entry.size).to be_nil
|
|
758
|
+
|
|
759
|
+
# Step 6 (FileActionFind) is for a Files.com site only.
|
|
760
|
+
|
|
761
|
+
# Step 7: upload hash-name from a local-file
|
|
762
|
+
upload_local_file(namespace.join("a#b.txt").to_s, recipe(4, 37, 5, 256, 1031, "29f483b8739ae2de994d4794133a7a858a0f0416caa03e5a96165af41ebdf3e8"))
|
|
763
|
+
|
|
764
|
+
# Step 8: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
765
|
+
entry, response = with_response { Files::File.find(namespace.join("a#b.txt").to_s, {}, options) }
|
|
766
|
+
expect(response.http_status).to eq(200)
|
|
767
|
+
expect(entry).to be_a(Files::File)
|
|
768
|
+
expect(entry.type).to eq("file")
|
|
769
|
+
expect(entry.path).to eq(namespace.join("a#b.txt").to_s)
|
|
770
|
+
expect(entry.size).to be_an(Integer).and eq(1031)
|
|
771
|
+
|
|
772
|
+
# Step 9: upload percent-name from a local-file
|
|
773
|
+
upload_local_file(namespace.join("100%.txt").to_s, recipe(5, 41, 11, 256, 2053, "83298104fe01d905ee9ce8fae2f23da08007b5e8ab356d5bfc064b49c2664b98"))
|
|
774
|
+
|
|
775
|
+
# Step 10: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
776
|
+
entry, response = with_response { Files::File.find(namespace.join("100%.txt").to_s, {}, options) }
|
|
777
|
+
expect(response.http_status).to eq(200)
|
|
778
|
+
expect(entry).to be_a(Files::File)
|
|
779
|
+
expect(entry.type).to eq("file")
|
|
780
|
+
expect(entry.path).to eq(namespace.join("100%.txt").to_s)
|
|
781
|
+
expect(entry.size).to be_an(Integer).and eq(2053)
|
|
782
|
+
|
|
783
|
+
# Step 11: upload unicode-name from a local-file
|
|
784
|
+
upload_local_file(namespace.join("é +.txt").to_s, recipe(6, 43, 3, 256, 517, "b664357b3151e48ca4c5c6c452dd20d58293897f887536f6e0e0627c0d8cd25b"))
|
|
785
|
+
|
|
786
|
+
# Step 12: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
787
|
+
entry, response = with_response { Files::File.find(namespace.join("é +.txt").to_s, {}, options) }
|
|
788
|
+
expect(response.http_status).to eq(200)
|
|
789
|
+
expect(entry).to be_a(Files::File)
|
|
790
|
+
expect(entry.type).to eq("file")
|
|
791
|
+
expect(entry.path).to eq(namespace.join("é +.txt").to_s)
|
|
792
|
+
expect(entry.size).to be_an(Integer).and eq(517)
|
|
793
|
+
|
|
794
|
+
# Step 13: PostFoldersPath (POST /folders/{path}), 201
|
|
795
|
+
entry, response = with_response { Files::Folder.create(namespace.join("nested").to_s, { mkdir_parents: false }, options) }
|
|
796
|
+
expect(response.http_status).to eq(201)
|
|
797
|
+
expect(entry).to be_a(Files::File)
|
|
798
|
+
expect(entry.type).to eq("directory")
|
|
799
|
+
expect(entry.path).to eq(namespace.join("nested").to_s)
|
|
800
|
+
expect(entry.size).to be_nil
|
|
801
|
+
|
|
802
|
+
# Step 14: upload inner.bin from a seekable-stream
|
|
803
|
+
upload_stream(namespace.join("nested/inner.bin").to_s, recipe(7, 47, 19, 256, 4099, "9f251477458622d9da868e31b8708ec638d8ba9a01c9b575bb8c0a0764734521"))
|
|
804
|
+
|
|
805
|
+
# Step 15: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
806
|
+
entry, response = with_response { Files::File.find(namespace.join("nested/inner.bin").to_s, {}, options) }
|
|
807
|
+
expect(response.http_status).to eq(200)
|
|
808
|
+
expect(entry).to be_a(Files::File)
|
|
809
|
+
expect(entry.type).to eq("file")
|
|
810
|
+
expect(entry.path).to eq(namespace.join("nested/inner.bin").to_s)
|
|
811
|
+
expect(entry.size).to be_an(Integer).and eq(4099)
|
|
812
|
+
|
|
813
|
+
# Step 16: upload large.bin from a local-file
|
|
814
|
+
upload_local_file(namespace.join("large.bin").to_s, recipe(8, 53, 23, 251, 6_291_459, "4ab1615925cb4b1fa9b9ddc3345a920200888d8ba7d3ae1b880aa6c0c19f87fa"), min_parts: 2, progress_final: true)
|
|
815
|
+
|
|
816
|
+
# Step 17: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
817
|
+
entry, response = with_response { Files::File.find(namespace.join("large.bin").to_s, {}, options) }
|
|
818
|
+
expect(response.http_status).to eq(200)
|
|
819
|
+
expect(entry).to be_a(Files::File)
|
|
820
|
+
expect(entry.type).to eq("file")
|
|
821
|
+
expect(entry.path).to eq(namespace.join("large.bin").to_s)
|
|
822
|
+
expect(entry.size).to be_an(Integer).and eq(6_291_459)
|
|
823
|
+
|
|
824
|
+
# Step 18: copy inner.bin to copied.bin
|
|
825
|
+
action = Files::File.copy(namespace.join("nested/inner.bin").to_s, { destination: namespace.join("copied.bin").to_s }, options)
|
|
826
|
+
expect(migrated(action) { |id| Files::FileMigration.find(id, {}, options) }).to eq("completed")
|
|
827
|
+
|
|
828
|
+
# Step 19: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
829
|
+
entry, response = with_response { Files::File.find(namespace.join("copied.bin").to_s, {}, options) }
|
|
830
|
+
expect(response.http_status).to eq(200)
|
|
831
|
+
expect(entry).to be_a(Files::File)
|
|
832
|
+
expect(entry.type).to eq("file")
|
|
833
|
+
expect(entry.path).to eq(namespace.join("copied.bin").to_s)
|
|
834
|
+
expect(entry.size).to be_an(Integer).and eq(4099)
|
|
835
|
+
|
|
836
|
+
# Step 20: move copied.bin to moved.bin
|
|
837
|
+
action = Files::File.move(namespace.join("copied.bin").to_s, { destination: namespace.join("nested/moved.bin").to_s }, options)
|
|
838
|
+
expect(migrated(action) { |id| Files::FileMigration.find(id, {}, options) }).to eq("completed")
|
|
839
|
+
|
|
840
|
+
# Step 21: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
841
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("copied.bin").to_s, {}, options) }
|
|
842
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
843
|
+
expect(error.http_status).to eq(404)
|
|
844
|
+
expect(error.type).to eq("not-found")
|
|
845
|
+
|
|
846
|
+
# Step 22: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
847
|
+
entry, response = with_response { Files::File.find(namespace.join("nested/moved.bin").to_s, {}, options) }
|
|
848
|
+
expect(response.http_status).to eq(200)
|
|
849
|
+
expect(entry).to be_a(Files::File)
|
|
850
|
+
expect(entry.type).to eq("file")
|
|
851
|
+
expect(entry.path).to eq(namespace.join("nested/moved.bin").to_s)
|
|
852
|
+
expect(entry.size).to be_an(Integer).and eq(4099)
|
|
853
|
+
|
|
854
|
+
# Step 23: FolderListForPath (GET /folders/{path}), 200
|
|
855
|
+
before = journal_seq
|
|
856
|
+
pages = pages_of(16) { Files::Folder.list_for(namespace.to_s, { per_page: 1 }, options) }
|
|
857
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
858
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1)
|
|
859
|
+
cursor_chain(before, pages.size, 200)
|
|
860
|
+
records = pages.flat_map(&:first)
|
|
861
|
+
expect(records).to all(be_a(Files::File))
|
|
862
|
+
expect(records.map { |record| [ record.path, record.type ] }).to contain_exactly(
|
|
863
|
+
[ namespace.join("a#b.txt").to_s, "file" ],
|
|
864
|
+
[ namespace.join("100%.txt").to_s, "file" ],
|
|
865
|
+
[ namespace.join("é +.txt").to_s, "file" ],
|
|
866
|
+
[ namespace.join("nested").to_s, "directory" ],
|
|
867
|
+
[ namespace.join("large.bin").to_s, "file" ],
|
|
868
|
+
)
|
|
869
|
+
records.each { |record| listed[record.path] = record }
|
|
870
|
+
|
|
871
|
+
# Step 24: download hash-name
|
|
872
|
+
download(namespace.join("a#b.txt").to_s, recipe(4, 37, 5, 256, 1031, "29f483b8739ae2de994d4794133a7a858a0f0416caa03e5a96165af41ebdf3e8"), listed_entry: true)
|
|
873
|
+
|
|
874
|
+
# Step 25: download percent-name
|
|
875
|
+
download(namespace.join("100%.txt").to_s, recipe(5, 41, 11, 256, 2053, "83298104fe01d905ee9ce8fae2f23da08007b5e8ab356d5bfc064b49c2664b98"), listed_entry: true)
|
|
876
|
+
|
|
877
|
+
# Step 26: download unicode-name
|
|
878
|
+
download(namespace.join("é +.txt").to_s, recipe(6, 43, 3, 256, 517, "b664357b3151e48ca4c5c6c452dd20d58293897f887536f6e0e0627c0d8cd25b"), listed_entry: true)
|
|
879
|
+
|
|
880
|
+
# Step 27: download large.bin
|
|
881
|
+
download(namespace.join("large.bin").to_s, recipe(8, 53, 23, 251, 6_291_459, "4ab1615925cb4b1fa9b9ddc3345a920200888d8ba7d3ae1b880aa6c0c19f87fa"), listed_entry: true)
|
|
882
|
+
|
|
883
|
+
# Step 28: download inner.bin
|
|
884
|
+
download(namespace.join("nested/inner.bin").to_s, recipe(7, 47, 19, 256, 4099, "9f251477458622d9da868e31b8708ec638d8ba9a01c9b575bb8c0a0764734521"))
|
|
885
|
+
|
|
886
|
+
# Step 29: download moved.bin
|
|
887
|
+
download(namespace.join("nested/moved.bin").to_s, recipe(7, 47, 19, 256, 4099, "9f251477458622d9da868e31b8708ec638d8ba9a01c9b575bb8c0a0764734521"))
|
|
888
|
+
|
|
889
|
+
# Step 30: download large.bin
|
|
890
|
+
download(namespace.join("large.bin").to_s, recipe(8, 53, 23, 251, 6_291_459, "4ab1615925cb4b1fa9b9ddc3345a920200888d8ba7d3ae1b880aa6c0c19f87fa"))
|
|
891
|
+
|
|
892
|
+
# Step 31: upload cancelled.bin from a local-file
|
|
893
|
+
upload_cancelled(namespace.join("cancelled.bin").to_s, recipe(9, 59, 29, 251, 6_291_461, "d7a71574a24faecb2fea4915b8096a86a43b19db98d440f16730caac0a66cfd4"))
|
|
894
|
+
|
|
895
|
+
# Step 32: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
896
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("cancelled.bin").to_s, {}, options) }
|
|
897
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
898
|
+
expect(error.http_status).to eq(404)
|
|
899
|
+
expect(error.type).to eq("not-found")
|
|
900
|
+
|
|
901
|
+
# Step 33: DeleteFilesPath (DELETE /files/{path}), 204
|
|
902
|
+
result, response = with_response { Files::File.delete(namespace.join("nested/moved.bin").to_s, { recursive: false }, options) }
|
|
903
|
+
expect(response.http_status).to eq(204)
|
|
904
|
+
expect(result).to be_nil
|
|
905
|
+
|
|
906
|
+
# Step 34: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
907
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("nested/moved.bin").to_s, {}, options) }
|
|
908
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
909
|
+
expect(error.http_status).to eq(404)
|
|
910
|
+
expect(error.type).to eq("not-found")
|
|
911
|
+
|
|
912
|
+
# Step 35: DeleteFilesPath (DELETE /files/{path}), 204
|
|
913
|
+
result, response = with_response { Files::File.delete(namespace.join("nested/inner.bin").to_s, { recursive: false }, options) }
|
|
914
|
+
expect(response.http_status).to eq(204)
|
|
915
|
+
expect(result).to be_nil
|
|
916
|
+
|
|
917
|
+
# Step 36: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
918
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("nested/inner.bin").to_s, {}, options) }
|
|
919
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
920
|
+
expect(error.http_status).to eq(404)
|
|
921
|
+
expect(error.type).to eq("not-found")
|
|
922
|
+
|
|
923
|
+
# Step 37: DeleteFilesPath (DELETE /files/{path}), 204
|
|
924
|
+
result, response = with_response { Files::File.delete(namespace.join("nested").to_s, { recursive: false }, options) }
|
|
925
|
+
expect(response.http_status).to eq(204)
|
|
926
|
+
expect(result).to be_nil
|
|
927
|
+
|
|
928
|
+
# Step 38: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
929
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("nested").to_s, {}, options) }
|
|
930
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
931
|
+
expect(error.http_status).to eq(404)
|
|
932
|
+
expect(error.type).to eq("not-found")
|
|
933
|
+
|
|
934
|
+
# Step 39: DeleteFilesPath (DELETE /files/{path}), 204
|
|
935
|
+
result, response = with_response { Files::File.delete(namespace.join("large.bin").to_s, { recursive: false }, options) }
|
|
936
|
+
expect(response.http_status).to eq(204)
|
|
937
|
+
expect(result).to be_nil
|
|
938
|
+
|
|
939
|
+
# Step 40: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
940
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("large.bin").to_s, {}, options) }
|
|
941
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
942
|
+
expect(error.http_status).to eq(404)
|
|
943
|
+
expect(error.type).to eq("not-found")
|
|
944
|
+
|
|
945
|
+
# Step 41: DeleteFilesPath (DELETE /files/{path}), 204
|
|
946
|
+
result, response = with_response { Files::File.delete(namespace.join("a#b.txt").to_s, { recursive: false }, options) }
|
|
947
|
+
expect(response.http_status).to eq(204)
|
|
948
|
+
expect(result).to be_nil
|
|
949
|
+
|
|
950
|
+
# Step 42: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
951
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("a#b.txt").to_s, {}, options) }
|
|
952
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
953
|
+
expect(error.http_status).to eq(404)
|
|
954
|
+
expect(error.type).to eq("not-found")
|
|
955
|
+
|
|
956
|
+
# Step 43: DeleteFilesPath (DELETE /files/{path}), 204
|
|
957
|
+
result, response = with_response { Files::File.delete(namespace.join("100%.txt").to_s, { recursive: false }, options) }
|
|
958
|
+
expect(response.http_status).to eq(204)
|
|
959
|
+
expect(result).to be_nil
|
|
960
|
+
|
|
961
|
+
# Step 44: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
962
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("100%.txt").to_s, {}, options) }
|
|
963
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
964
|
+
expect(error.http_status).to eq(404)
|
|
965
|
+
expect(error.type).to eq("not-found")
|
|
966
|
+
|
|
967
|
+
# Step 45: DeleteFilesPath (DELETE /files/{path}), 204
|
|
968
|
+
result, response = with_response { Files::File.delete(namespace.join("é +.txt").to_s, { recursive: false }, options) }
|
|
969
|
+
expect(response.http_status).to eq(204)
|
|
970
|
+
expect(result).to be_nil
|
|
971
|
+
|
|
972
|
+
# Step 46: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
973
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("é +.txt").to_s, {}, options) }
|
|
974
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
975
|
+
expect(error.http_status).to eq(404)
|
|
976
|
+
expect(error.type).to eq("not-found")
|
|
977
|
+
|
|
978
|
+
# Step 47: DeleteFilesPath (DELETE /files/{path}), 204
|
|
979
|
+
result, response = with_response { Files::File.delete(namespace.to_s, { recursive: false }, options) }
|
|
980
|
+
expect(response.http_status).to eq(204)
|
|
981
|
+
expect(result).to be_nil
|
|
982
|
+
|
|
983
|
+
# Step 48: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
984
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.to_s, {}, options) }
|
|
985
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
986
|
+
expect(error.http_status).to eq(404)
|
|
987
|
+
expect(error.type).to eq("not-found")
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
it "files.owned-directory-helpers: A local tree uploaded as a directory, each entry it made found and each folder listed with exactly its own entries, the tree downloaded as a directory byte for byte, a folder copied and the copy moved (each waited for), the moved file downloaded, and non-recursive deletes, deepest first, confirmed by absence. Conserved from verification/files-paired/contract-helpers.json revision 1 and its journeys." do
|
|
991
|
+
# Step 1: FileActionFind (GET /file_actions/metadata/{path}), 422 bad-request/request-params-required
|
|
992
|
+
expect { Files::File.find("/", {}, options) }.to raise_error(Files::RequestParamsRequiredError) { |error|
|
|
993
|
+
expect(error.http_status).to eq(422)
|
|
994
|
+
expect(error.type).to eq("bad-request/request-params-required")
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
# Step 2: FolderListForPath (GET /folders/{path}), 200
|
|
998
|
+
before = journal_seq
|
|
999
|
+
pages = pages_of(16) { Files::Folder.list_for("/", { per_page: 1000 }, options) }
|
|
1000
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
1001
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
1002
|
+
cursor_chain(before, pages.size, 200)
|
|
1003
|
+
records = pages.flat_map(&:first)
|
|
1004
|
+
expect(records).to all(be_a(Files::File))
|
|
1005
|
+
# The listing's members (the root's) are the site's: its pages are checked, not its members.
|
|
1006
|
+
|
|
1007
|
+
# Step 3 (BehaviorListForPath) is for a Files.com site only.
|
|
1008
|
+
|
|
1009
|
+
# Step 4: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1010
|
+
expect { Files::File.find(namespace.to_s, {}, options) }.to raise_error(Files::NotFoundError) { |error|
|
|
1011
|
+
expect(error.http_status).to eq(404)
|
|
1012
|
+
expect(error.type).to eq("not-found")
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
# Step 5: PostFoldersPath (POST /folders/{path}), 201
|
|
1016
|
+
entry, response = with_response { Files::Folder.create(namespace.to_s, { mkdir_parents: false }, options) }
|
|
1017
|
+
expect(response.http_status).to eq(201)
|
|
1018
|
+
expect(entry).to be_a(Files::File)
|
|
1019
|
+
expect(entry.type).to eq("directory")
|
|
1020
|
+
expect(entry.path).to eq(namespace.to_s)
|
|
1021
|
+
expect(entry.size).to be_nil
|
|
1022
|
+
|
|
1023
|
+
# Step 6 (FileActionFind) is for a Files.com site only.
|
|
1024
|
+
|
|
1025
|
+
# Step 7: upload-directory up
|
|
1026
|
+
tree = [
|
|
1027
|
+
[ "a.bin", recipe(10, 61, 7, 256, 1543, "983f31356cea338492c47d6c9c979deb3a45914fe6aa8eafa96cd44e02f08e9d") ],
|
|
1028
|
+
[ "sub", nil ],
|
|
1029
|
+
[ "sub/b.bin", recipe(11, 67, 13, 256, 2089, "03e528690696066e61d8a65fa0fac38974bb95cb0d31874240287f325484fddb") ],
|
|
1030
|
+
[ "empty", nil ]
|
|
1031
|
+
]
|
|
1032
|
+
upload_directory(namespace.join("up").to_s, tree, completed: [ "a.bin", "sub/b.bin" ], files: 2, bytes: 3632, directories: [ "empty", "sub" ], empty_directories: [ "empty" ], empty_folders: 1) { |path| Files::Folder.create(path, {}, options) }
|
|
1033
|
+
|
|
1034
|
+
# Step 8: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
1035
|
+
entry, response = with_response { Files::File.find(namespace.join("up").to_s, {}, options) }
|
|
1036
|
+
expect(response.http_status).to eq(200)
|
|
1037
|
+
expect(entry).to be_a(Files::File)
|
|
1038
|
+
expect(entry.type).to eq("directory")
|
|
1039
|
+
expect(entry.path).to eq(namespace.join("up").to_s)
|
|
1040
|
+
expect(entry.size).to be_nil
|
|
1041
|
+
|
|
1042
|
+
# Step 9: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
1043
|
+
entry, response = with_response { Files::File.find(namespace.join("up/a.bin").to_s, {}, options) }
|
|
1044
|
+
expect(response.http_status).to eq(200)
|
|
1045
|
+
expect(entry).to be_a(Files::File)
|
|
1046
|
+
expect(entry.type).to eq("file")
|
|
1047
|
+
expect(entry.path).to eq(namespace.join("up/a.bin").to_s)
|
|
1048
|
+
expect(entry.size).to be_an(Integer).and eq(1543)
|
|
1049
|
+
|
|
1050
|
+
# Step 10: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
1051
|
+
entry, response = with_response { Files::File.find(namespace.join("up/sub").to_s, {}, options) }
|
|
1052
|
+
expect(response.http_status).to eq(200)
|
|
1053
|
+
expect(entry).to be_a(Files::File)
|
|
1054
|
+
expect(entry.type).to eq("directory")
|
|
1055
|
+
expect(entry.path).to eq(namespace.join("up/sub").to_s)
|
|
1056
|
+
expect(entry.size).to be_nil
|
|
1057
|
+
|
|
1058
|
+
# Step 11: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
1059
|
+
entry, response = with_response { Files::File.find(namespace.join("up/sub/b.bin").to_s, {}, options) }
|
|
1060
|
+
expect(response.http_status).to eq(200)
|
|
1061
|
+
expect(entry).to be_a(Files::File)
|
|
1062
|
+
expect(entry.type).to eq("file")
|
|
1063
|
+
expect(entry.path).to eq(namespace.join("up/sub/b.bin").to_s)
|
|
1064
|
+
expect(entry.size).to be_an(Integer).and eq(2089)
|
|
1065
|
+
|
|
1066
|
+
# Step 12: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
1067
|
+
entry, response = with_response { Files::File.find(namespace.join("up/empty").to_s, {}, options) }
|
|
1068
|
+
expect(response.http_status).to eq(200)
|
|
1069
|
+
expect(entry).to be_a(Files::File)
|
|
1070
|
+
expect(entry.type).to eq("directory")
|
|
1071
|
+
expect(entry.path).to eq(namespace.join("up/empty").to_s)
|
|
1072
|
+
expect(entry.size).to be_nil
|
|
1073
|
+
|
|
1074
|
+
# Step 13: FolderListForPath (GET /folders/{path}), 200
|
|
1075
|
+
before = journal_seq
|
|
1076
|
+
pages = pages_of(16) { Files::Folder.list_for(namespace.join("up").to_s, { per_page: 1000 }, options) }
|
|
1077
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
1078
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
1079
|
+
cursor_chain(before, pages.size, 200)
|
|
1080
|
+
records = pages.flat_map(&:first)
|
|
1081
|
+
expect(records).to all(be_a(Files::File))
|
|
1082
|
+
expect(records.map { |record| [ record.path, record.type ] }).to contain_exactly(
|
|
1083
|
+
[ namespace.join("up/a.bin").to_s, "file" ],
|
|
1084
|
+
[ namespace.join("up/sub").to_s, "directory" ],
|
|
1085
|
+
[ namespace.join("up/empty").to_s, "directory" ],
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
# Step 14: FolderListForPath (GET /folders/{path}), 200
|
|
1089
|
+
before = journal_seq
|
|
1090
|
+
pages = pages_of(16) { Files::Folder.list_for(namespace.join("up/sub").to_s, { per_page: 1000 }, options) }
|
|
1091
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
1092
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
1093
|
+
cursor_chain(before, pages.size, 200)
|
|
1094
|
+
records = pages.flat_map(&:first)
|
|
1095
|
+
expect(records).to all(be_a(Files::File))
|
|
1096
|
+
expect(records.map { |record| [ record.path, record.type ] }).to contain_exactly(
|
|
1097
|
+
[ namespace.join("up/sub/b.bin").to_s, "file" ],
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
# Step 15: FolderListForPath (GET /folders/{path}), 200
|
|
1101
|
+
before = journal_seq
|
|
1102
|
+
pages = pages_of(16) { Files::Folder.list_for(namespace.join("up/empty").to_s, { per_page: 1000 }, options) }
|
|
1103
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
1104
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
1105
|
+
cursor_chain(before, pages.size, 200)
|
|
1106
|
+
records = pages.flat_map(&:first)
|
|
1107
|
+
expect(records).to all(be_a(Files::File))
|
|
1108
|
+
expect(records).to be_empty
|
|
1109
|
+
|
|
1110
|
+
# Step 16: download-directory up
|
|
1111
|
+
matched = [
|
|
1112
|
+
[ "a.bin", recipe(10, 61, 7, 256, 1543, "983f31356cea338492c47d6c9c979deb3a45914fe6aa8eafa96cd44e02f08e9d") ],
|
|
1113
|
+
[ "sub/b.bin", recipe(11, 67, 13, 256, 2089, "03e528690696066e61d8a65fa0fac38974bb95cb0d31874240287f325484fddb") ]
|
|
1114
|
+
]
|
|
1115
|
+
download_directory(namespace.join("up").to_s, matched, files: 2, empty_folders: 1, bytes: 3632) { |path| Files::Folder.list_for(path, {}, options) }
|
|
1116
|
+
|
|
1117
|
+
# Step 17: copy up-sub to copy
|
|
1118
|
+
action = Files::File.copy(namespace.join("up/sub").to_s, { destination: namespace.join("copy").to_s }, options)
|
|
1119
|
+
expect(migrated(action) { |id| Files::FileMigration.find(id, {}, options) }).to eq("completed")
|
|
1120
|
+
|
|
1121
|
+
# Step 18: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
1122
|
+
entry, response = with_response { Files::File.find(namespace.join("copy").to_s, {}, options) }
|
|
1123
|
+
expect(response.http_status).to eq(200)
|
|
1124
|
+
expect(entry).to be_a(Files::File)
|
|
1125
|
+
expect(entry.type).to eq("directory")
|
|
1126
|
+
expect(entry.path).to eq(namespace.join("copy").to_s)
|
|
1127
|
+
expect(entry.size).to be_nil
|
|
1128
|
+
|
|
1129
|
+
# Step 19: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
1130
|
+
entry, response = with_response { Files::File.find(namespace.join("copy/b.bin").to_s, {}, options) }
|
|
1131
|
+
expect(response.http_status).to eq(200)
|
|
1132
|
+
expect(entry).to be_a(Files::File)
|
|
1133
|
+
expect(entry.type).to eq("file")
|
|
1134
|
+
expect(entry.path).to eq(namespace.join("copy/b.bin").to_s)
|
|
1135
|
+
expect(entry.size).to be_an(Integer).and eq(2089)
|
|
1136
|
+
|
|
1137
|
+
# Step 20: FolderListForPath (GET /folders/{path}), 200
|
|
1138
|
+
before = journal_seq
|
|
1139
|
+
pages = pages_of(16) { Files::Folder.list_for(namespace.join("copy").to_s, { per_page: 1000 }, options) }
|
|
1140
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
1141
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
1142
|
+
cursor_chain(before, pages.size, 200)
|
|
1143
|
+
records = pages.flat_map(&:first)
|
|
1144
|
+
expect(records).to all(be_a(Files::File))
|
|
1145
|
+
expect(records.map { |record| [ record.path, record.type ] }).to contain_exactly(
|
|
1146
|
+
[ namespace.join("copy/b.bin").to_s, "file" ],
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
# Step 21: move copy to moved
|
|
1150
|
+
action = Files::File.move(namespace.join("copy").to_s, { destination: namespace.join("moved").to_s }, options)
|
|
1151
|
+
expect(migrated(action) { |id| Files::FileMigration.find(id, {}, options) }).to eq("completed")
|
|
1152
|
+
|
|
1153
|
+
# Step 22: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1154
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("copy/b.bin").to_s, {}, options) }
|
|
1155
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
1156
|
+
expect(error.http_status).to eq(404)
|
|
1157
|
+
expect(error.type).to eq("not-found")
|
|
1158
|
+
|
|
1159
|
+
# Step 23: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1160
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("copy").to_s, {}, options) }
|
|
1161
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
1162
|
+
expect(error.http_status).to eq(404)
|
|
1163
|
+
expect(error.type).to eq("not-found")
|
|
1164
|
+
|
|
1165
|
+
# Step 24: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
1166
|
+
entry, response = with_response { Files::File.find(namespace.join("moved").to_s, {}, options) }
|
|
1167
|
+
expect(response.http_status).to eq(200)
|
|
1168
|
+
expect(entry).to be_a(Files::File)
|
|
1169
|
+
expect(entry.type).to eq("directory")
|
|
1170
|
+
expect(entry.path).to eq(namespace.join("moved").to_s)
|
|
1171
|
+
expect(entry.size).to be_nil
|
|
1172
|
+
|
|
1173
|
+
# Step 25: FileActionFind (GET /file_actions/metadata/{path}), 200
|
|
1174
|
+
entry, response = with_response { Files::File.find(namespace.join("moved/b.bin").to_s, {}, options) }
|
|
1175
|
+
expect(response.http_status).to eq(200)
|
|
1176
|
+
expect(entry).to be_a(Files::File)
|
|
1177
|
+
expect(entry.type).to eq("file")
|
|
1178
|
+
expect(entry.path).to eq(namespace.join("moved/b.bin").to_s)
|
|
1179
|
+
expect(entry.size).to be_an(Integer).and eq(2089)
|
|
1180
|
+
|
|
1181
|
+
# Step 26: FolderListForPath (GET /folders/{path}), 200
|
|
1182
|
+
before = journal_seq
|
|
1183
|
+
pages = pages_of(16) { Files::Folder.list_for(namespace.join("moved").to_s, { per_page: 1000 }, options) }
|
|
1184
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
1185
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
1186
|
+
cursor_chain(before, pages.size, 200)
|
|
1187
|
+
records = pages.flat_map(&:first)
|
|
1188
|
+
expect(records).to all(be_a(Files::File))
|
|
1189
|
+
expect(records.map { |record| [ record.path, record.type ] }).to contain_exactly(
|
|
1190
|
+
[ namespace.join("moved/b.bin").to_s, "file" ],
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
# Step 27: FolderListForPath (GET /folders/{path}), 200
|
|
1194
|
+
before = journal_seq
|
|
1195
|
+
pages = pages_of(16) { Files::Folder.list_for(namespace.to_s, { per_page: 1000 }, options) }
|
|
1196
|
+
expect(pages.map(&:last)).to all(eq(200))
|
|
1197
|
+
expect(pages.map { |page, _status| page.size }).to all(be <= 1000)
|
|
1198
|
+
cursor_chain(before, pages.size, 200)
|
|
1199
|
+
records = pages.flat_map(&:first)
|
|
1200
|
+
expect(records).to all(be_a(Files::File))
|
|
1201
|
+
expect(records.map { |record| [ record.path, record.type ] }).to contain_exactly(
|
|
1202
|
+
[ namespace.join("up").to_s, "directory" ],
|
|
1203
|
+
[ namespace.join("moved").to_s, "directory" ],
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
# Step 28: download moved-b
|
|
1207
|
+
download(namespace.join("moved/b.bin").to_s, recipe(11, 67, 13, 256, 2089, "03e528690696066e61d8a65fa0fac38974bb95cb0d31874240287f325484fddb"))
|
|
1208
|
+
|
|
1209
|
+
# Step 29: DeleteFilesPath (DELETE /files/{path}), 204
|
|
1210
|
+
result, response = with_response { Files::File.delete(namespace.join("moved/b.bin").to_s, { recursive: false }, options) }
|
|
1211
|
+
expect(response.http_status).to eq(204)
|
|
1212
|
+
expect(result).to be_nil
|
|
1213
|
+
|
|
1214
|
+
# Step 30: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1215
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("moved/b.bin").to_s, {}, options) }
|
|
1216
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
1217
|
+
expect(error.http_status).to eq(404)
|
|
1218
|
+
expect(error.type).to eq("not-found")
|
|
1219
|
+
|
|
1220
|
+
# Step 31: DeleteFilesPath (DELETE /files/{path}), 204
|
|
1221
|
+
result, response = with_response { Files::File.delete(namespace.join("moved").to_s, { recursive: false }, options) }
|
|
1222
|
+
expect(response.http_status).to eq(204)
|
|
1223
|
+
expect(result).to be_nil
|
|
1224
|
+
|
|
1225
|
+
# Step 32: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1226
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("moved").to_s, {}, options) }
|
|
1227
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
1228
|
+
expect(error.http_status).to eq(404)
|
|
1229
|
+
expect(error.type).to eq("not-found")
|
|
1230
|
+
|
|
1231
|
+
# Step 33: DeleteFilesPath (DELETE /files/{path}), 204
|
|
1232
|
+
result, response = with_response { Files::File.delete(namespace.join("up/sub/b.bin").to_s, { recursive: false }, options) }
|
|
1233
|
+
expect(response.http_status).to eq(204)
|
|
1234
|
+
expect(result).to be_nil
|
|
1235
|
+
|
|
1236
|
+
# Step 34: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1237
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("up/sub/b.bin").to_s, {}, options) }
|
|
1238
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
1239
|
+
expect(error.http_status).to eq(404)
|
|
1240
|
+
expect(error.type).to eq("not-found")
|
|
1241
|
+
|
|
1242
|
+
# Step 35: DeleteFilesPath (DELETE /files/{path}), 204
|
|
1243
|
+
result, response = with_response { Files::File.delete(namespace.join("up/sub").to_s, { recursive: false }, options) }
|
|
1244
|
+
expect(response.http_status).to eq(204)
|
|
1245
|
+
expect(result).to be_nil
|
|
1246
|
+
|
|
1247
|
+
# Step 36: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1248
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("up/sub").to_s, {}, options) }
|
|
1249
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
1250
|
+
expect(error.http_status).to eq(404)
|
|
1251
|
+
expect(error.type).to eq("not-found")
|
|
1252
|
+
|
|
1253
|
+
# Step 37: DeleteFilesPath (DELETE /files/{path}), 204
|
|
1254
|
+
result, response = with_response { Files::File.delete(namespace.join("up/empty").to_s, { recursive: false }, options) }
|
|
1255
|
+
expect(response.http_status).to eq(204)
|
|
1256
|
+
expect(result).to be_nil
|
|
1257
|
+
|
|
1258
|
+
# Step 38: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1259
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("up/empty").to_s, {}, options) }
|
|
1260
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
1261
|
+
expect(error.http_status).to eq(404)
|
|
1262
|
+
expect(error.type).to eq("not-found")
|
|
1263
|
+
|
|
1264
|
+
# Step 39: DeleteFilesPath (DELETE /files/{path}), 204
|
|
1265
|
+
result, response = with_response { Files::File.delete(namespace.join("up/a.bin").to_s, { recursive: false }, options) }
|
|
1266
|
+
expect(response.http_status).to eq(204)
|
|
1267
|
+
expect(result).to be_nil
|
|
1268
|
+
|
|
1269
|
+
# Step 40: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1270
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("up/a.bin").to_s, {}, options) }
|
|
1271
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
1272
|
+
expect(error.http_status).to eq(404)
|
|
1273
|
+
expect(error.type).to eq("not-found")
|
|
1274
|
+
|
|
1275
|
+
# Step 41: DeleteFilesPath (DELETE /files/{path}), 204
|
|
1276
|
+
result, response = with_response { Files::File.delete(namespace.join("up").to_s, { recursive: false }, options) }
|
|
1277
|
+
expect(response.http_status).to eq(204)
|
|
1278
|
+
expect(result).to be_nil
|
|
1279
|
+
|
|
1280
|
+
# Step 42: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1281
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.join("up").to_s, {}, options) }
|
|
1282
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
1283
|
+
expect(error.http_status).to eq(404)
|
|
1284
|
+
expect(error.type).to eq("not-found")
|
|
1285
|
+
|
|
1286
|
+
# Step 43: DeleteFilesPath (DELETE /files/{path}), 204
|
|
1287
|
+
result, response = with_response { Files::File.delete(namespace.to_s, { recursive: false }, options) }
|
|
1288
|
+
expect(response.http_status).to eq(204)
|
|
1289
|
+
expect(result).to be_nil
|
|
1290
|
+
|
|
1291
|
+
# Step 44: FileActionFind (GET /file_actions/metadata/{path}), 404 not-found
|
|
1292
|
+
error = eventually_raised(Files::NotFoundError, 5, 1) { Files::File.find(namespace.to_s, {}, options) }
|
|
1293
|
+
expect(error).to be_a(Files::NotFoundError)
|
|
1294
|
+
expect(error.http_status).to eq(404)
|
|
1295
|
+
expect(error.type).to eq("not-found")
|
|
1296
|
+
end
|
|
1297
|
+
end
|
|
1298
|
+
end
|