files.com 1.1.794 → 1.1.796

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.
@@ -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