files.com 1.1.785 → 1.1.787
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/README.md +2 -2
- data/_VERSION +1 -1
- data/lib/files.com/api.rb +20 -5
- data/lib/files.com/api_client.rb +196 -17
- data/lib/files.com/list.rb +4 -2
- data/lib/files.com/models/action_log.rb +4 -1
- data/lib/files.com/models/action_notification_export.rb +3 -1
- data/lib/files.com/models/action_notification_export_result.rb +4 -1
- data/lib/files.com/models/ai_assistant_personality.rb +20 -7
- data/lib/files.com/models/ai_task.rb +22 -10
- data/lib/files.com/models/api_key.rb +15 -7
- data/lib/files.com/models/api_request_log.rb +4 -1
- data/lib/files.com/models/app.rb +4 -1
- data/lib/files.com/models/as2_incoming_message.rb +4 -1
- data/lib/files.com/models/as2_outgoing_message.rb +4 -1
- data/lib/files.com/models/as2_partner.rb +18 -7
- data/lib/files.com/models/as2_station.rb +14 -7
- data/lib/files.com/models/automation.rb +47 -13
- data/lib/files.com/models/automation_log.rb +4 -1
- data/lib/files.com/models/automation_run.rb +16 -9
- data/lib/files.com/models/bandwidth_snapshot.rb +4 -1
- data/lib/files.com/models/behavior.rb +27 -9
- data/lib/files.com/models/bundle.rb +57 -10
- data/lib/files.com/models/bundle_action.rb +4 -1
- data/lib/files.com/models/bundle_download.rb +4 -1
- data/lib/files.com/models/bundle_notification.rb +20 -7
- data/lib/files.com/models/bundle_recipient.rb +6 -1
- data/lib/files.com/models/bundle_registration.rb +4 -1
- data/lib/files.com/models/chat_session.rb +5 -2
- data/lib/files.com/models/child_site_management_policy.rb +18 -7
- data/lib/files.com/models/clickwrap.rb +14 -7
- data/lib/files.com/models/custom_domain.rb +18 -8
- data/lib/files.com/models/desktop_configuration_profile.rb +21 -7
- data/lib/files.com/models/dns_record.rb +4 -1
- data/lib/files.com/models/email_incoming_message.rb +4 -1
- data/lib/files.com/models/email_log.rb +4 -1
- data/lib/files.com/models/event_channel.rb +20 -7
- data/lib/files.com/models/event_delivery_attempt.rb +5 -2
- data/lib/files.com/models/event_record.rb +5 -2
- data/lib/files.com/models/event_subscription.rb +25 -7
- data/lib/files.com/models/event_target.rb +22 -7
- data/lib/files.com/models/exavault_api_request_log.rb +4 -1
- data/lib/files.com/models/expectation.rb +23 -10
- data/lib/files.com/models/expectation_evaluation.rb +5 -2
- data/lib/files.com/models/expectation_incident.rb +20 -11
- data/lib/files.com/models/external_event.rb +6 -2
- data/lib/files.com/models/file.rb +110 -37
- data/lib/files.com/models/file_comment.rb +14 -7
- data/lib/files.com/models/file_comment_reaction.rb +6 -3
- data/lib/files.com/models/file_migration.rb +1 -1
- data/lib/files.com/models/file_migration_log.rb +4 -1
- data/lib/files.com/models/folder.rb +11 -3
- data/lib/files.com/models/form_field_set.rb +26 -7
- data/lib/files.com/models/ftp_action_log.rb +4 -1
- data/lib/files.com/models/gpg_key.rb +15 -7
- data/lib/files.com/models/group.rb +30 -7
- data/lib/files.com/models/group_user.rb +16 -6
- data/lib/files.com/models/history.rb +23 -8
- data/lib/files.com/models/history_export.rb +2 -1
- data/lib/files.com/models/history_export_result.rb +4 -1
- data/lib/files.com/models/holiday_calendar.rb +15 -7
- data/lib/files.com/models/holiday_region.rb +4 -1
- data/lib/files.com/models/inbound_s3_log.rb +4 -1
- data/lib/files.com/models/inbox_recipient.rb +6 -1
- data/lib/files.com/models/inbox_registration.rb +4 -1
- data/lib/files.com/models/inbox_upload.rb +4 -1
- data/lib/files.com/models/integration_centric_profile.rb +17 -7
- data/lib/files.com/models/invoice.rb +5 -2
- data/lib/files.com/models/ip_address.rb +16 -4
- data/lib/files.com/models/key_lifecycle_rule.rb +17 -7
- data/lib/files.com/models/lock.rb +16 -6
- data/lib/files.com/models/metadata_category.rb +19 -9
- data/lib/files.com/models/notification.rb +39 -7
- data/lib/files.com/models/outbound_connection_log.rb +4 -1
- data/lib/files.com/models/partner.rb +32 -7
- data/lib/files.com/models/partner_channel.rb +17 -7
- data/lib/files.com/models/partner_channel_template.rb +17 -7
- data/lib/files.com/models/partner_site.rb +5 -3
- data/lib/files.com/models/partner_site_request.rb +10 -4
- data/lib/files.com/models/payment.rb +5 -2
- data/lib/files.com/models/pending_work_event.rb +5 -2
- data/lib/files.com/models/permission.rb +12 -4
- data/lib/files.com/models/public_hosting_request_log.rb +4 -1
- data/lib/files.com/models/public_key.rb +15 -7
- data/lib/files.com/models/remote_bandwidth_snapshot.rb +4 -1
- data/lib/files.com/models/remote_mount_backend.rb +25 -10
- data/lib/files.com/models/remote_server.rb +46 -14
- data/lib/files.com/models/remote_server_credential.rb +14 -7
- data/lib/files.com/models/request.rb +17 -6
- data/lib/files.com/models/restore.rb +8 -1
- data/lib/files.com/models/schedule.rb +14 -7
- data/lib/files.com/models/scheduled_export.rb +18 -7
- data/lib/files.com/models/scim_log.rb +5 -2
- data/lib/files.com/models/secret.rb +15 -7
- data/lib/files.com/models/session.rb +1 -0
- data/lib/files.com/models/settings_change.rb +4 -1
- data/lib/files.com/models/sftp_action_log.rb +4 -1
- data/lib/files.com/models/sftp_host_key.rb +17 -7
- data/lib/files.com/models/share_group.rb +14 -7
- data/lib/files.com/models/siem_http_destination.rb +67 -7
- data/lib/files.com/models/siem_http_destination_event.rb +5 -2
- data/lib/files.com/models/site.rb +109 -0
- data/lib/files.com/models/site_subdomain_redirect.rb +10 -5
- data/lib/files.com/models/snapshot.rb +19 -10
- data/lib/files.com/models/sso_event.rb +5 -2
- data/lib/files.com/models/sso_strategy.rb +10 -5
- data/lib/files.com/models/style.rb +9 -6
- data/lib/files.com/models/sync.rb +36 -13
- data/lib/files.com/models/sync_log.rb +4 -1
- data/lib/files.com/models/sync_run.rb +5 -2
- data/lib/files.com/models/usage_daily_snapshot.rb +4 -1
- data/lib/files.com/models/usage_snapshot.rb +4 -1
- data/lib/files.com/models/user.rb +124 -16
- data/lib/files.com/models/user_additional_email_recipient.rb +14 -7
- data/lib/files.com/models/user_cipher_use.rb +4 -1
- data/lib/files.com/models/user_lifecycle_rule.rb +26 -7
- data/lib/files.com/models/user_request.rb +11 -5
- data/lib/files.com/models/user_security_event.rb +5 -2
- data/lib/files.com/models/user_sftp_client_use.rb +4 -1
- data/lib/files.com/models/web_dav_action_log.rb +4 -1
- data/lib/files.com/models/webhook_test.rb +3 -0
- data/lib/files.com/models/workspace.rb +14 -7
- data/lib/files.com/util.rb +32 -1
- data/lib/files.com/version.rb +3 -1
- data/lib/files.com.rb +54 -1
- data/spec/lib/api_client_spec.rb +492 -0
- data/spec/list_spec.rb +31 -2
- data/spec/models/persistence_spec.rb +1 -1
- data/spec/models/user_spec.rb +20 -0
- metadata +2 -1
data/spec/lib/api_client_spec.rb
CHANGED
|
@@ -400,6 +400,59 @@ RSpec.describe Files::ApiClient do
|
|
|
400
400
|
expect { Files::Workspace.find(5, {}, api_key: "key", client: quiet_client) }.not_to output.to_stdout
|
|
401
401
|
expect(global_output.string).not_to include("/workspaces/5")
|
|
402
402
|
end
|
|
403
|
+
|
|
404
|
+
it "writes each request's and response's headers to a DEBUG logger with their other details" do
|
|
405
|
+
sent_markers = []
|
|
406
|
+
connection = described_class.build_default_conn
|
|
407
|
+
connection.adapter :test do |stub|
|
|
408
|
+
stub.get("/api/rest/v1/bundles/83") do |env|
|
|
409
|
+
sent_markers << env.request_headers["X-Request-Marker"]
|
|
410
|
+
json_response(200, { id: 83, code: "debug-response-body" }, "X-Response-Marker" => "debug-response-header")
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
output = StringIO.new
|
|
414
|
+
client = described_class.new(connection, logger: Logger.new(output, level: Logger::DEBUG))
|
|
415
|
+
|
|
416
|
+
bundle = Files::Bundle.find(83, {}, api_key: "key", client: client, "X-Request-Marker" => "debug-request-header")
|
|
417
|
+
|
|
418
|
+
expect([ bundle.id, bundle.attributes[:code] ]).to eq([ 83, "debug-response-body" ])
|
|
419
|
+
expect(sent_markers).to eq([ "debug-request-header" ])
|
|
420
|
+
expect(output.string.lines.grep(/message=Request details/).join).to include("debug-request-header")
|
|
421
|
+
expect(output.string.lines.grep(/message=Response details/).join).to include("debug-response-header", "debug-response-body")
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
it "logs each retry's attempt, reason and planned wait, and no retry once the budget is spent" do
|
|
425
|
+
original_delays = [ Files.initial_network_retry_delay, Files.max_network_retry_delay ]
|
|
426
|
+
Files.max_network_retries = 1
|
|
427
|
+
Files.initial_network_retry_delay = 0
|
|
428
|
+
Files.max_network_retry_delay = 0
|
|
429
|
+
statuses = { 1 => [ 503, 200 ], 2 => [ 503, 503 ] }
|
|
430
|
+
connection = described_class.build_default_conn
|
|
431
|
+
connection.adapter :test do |stub|
|
|
432
|
+
statuses.each_key do |id|
|
|
433
|
+
stub.get("/api/rest/v1/workspaces/#{id}") do
|
|
434
|
+
status = statuses[id].shift
|
|
435
|
+
next json_response(200, { id: id, name: "Primary" }) if status == 200
|
|
436
|
+
|
|
437
|
+
json_response(status, { error: "HTTP #{status}" })
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
output = StringIO.new
|
|
442
|
+
client = described_class.new(connection, logger: Logger.new(output, level: Logger::INFO))
|
|
443
|
+
|
|
444
|
+
workspace = Files::Workspace.find(1, {}, api_key: "key", client: client)
|
|
445
|
+
expect { Files::Workspace.find(2, {}, api_key: "key", client: client) }.to raise_error(Files::APIError)
|
|
446
|
+
|
|
447
|
+
expect([ workspace.id, workspace.attributes[:name] ]).to eq([ 1, "Primary" ])
|
|
448
|
+
expect(statuses).to eq(1 => [], 2 => [])
|
|
449
|
+
retries = output.string.lines.grep(/message=Retry /)
|
|
450
|
+
expect(retries.size).to eq(2)
|
|
451
|
+
expect(retries).to all(include("attempt=1", "max_retries=1", "status=503", "wait_seconds=0"))
|
|
452
|
+
expect(retries.map { |line| line[/path=\S+/] }).to eq([ "path=/workspaces/1", "path=/workspaces/2" ])
|
|
453
|
+
ensure
|
|
454
|
+
Files.initial_network_retry_delay, Files.max_network_retry_delay = original_delays
|
|
455
|
+
end
|
|
403
456
|
end
|
|
404
457
|
|
|
405
458
|
describe "client workspaces" do
|
|
@@ -441,6 +494,151 @@ RSpec.describe Files::ApiClient do
|
|
|
441
494
|
end
|
|
442
495
|
end
|
|
443
496
|
|
|
497
|
+
describe "caller containers" do
|
|
498
|
+
around do |example|
|
|
499
|
+
original = Files.api_key
|
|
500
|
+
example.run
|
|
501
|
+
ensure
|
|
502
|
+
Files.api_key = original
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
it "finds a workspace without changing the caller's params or options" do
|
|
506
|
+
requests = []
|
|
507
|
+
connection = Faraday.new do |builder|
|
|
508
|
+
builder.response :raise_error
|
|
509
|
+
builder.adapter :test do |stub|
|
|
510
|
+
stub.get("/api/rest/v1/workspaces/1") do |env|
|
|
511
|
+
requests << [ env.method, env.url.path, env.url.query ]
|
|
512
|
+
[ 200, { "Content-Type" => "application/json" }, { id: 1, name: "Primary" }.to_json ]
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
client = described_class.new(connection)
|
|
517
|
+
params = {}.freeze
|
|
518
|
+
options = { api_key: "key", client: client }.freeze
|
|
519
|
+
|
|
520
|
+
workspace = Files::Workspace.find(1, params, options)
|
|
521
|
+
|
|
522
|
+
expect(params).to eq({})
|
|
523
|
+
expect(options).to eq(api_key: "key", client: client)
|
|
524
|
+
expect(requests).to eq([ [ :get, "/api/rest/v1/workspaces/1", "id=1" ] ])
|
|
525
|
+
expect(workspace).to be_a(Files::Workspace)
|
|
526
|
+
expect([ workspace.id, workspace.attributes[:name] ]).to eq([ 1, "Primary" ])
|
|
527
|
+
expect(workspace.options).to include(api_key: "key", client: client, session_id: nil)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
it "keeps the client and key a model's first successful request chose for its later requests" do
|
|
531
|
+
requests = { a: [], b: [] }
|
|
532
|
+
clients = requests.keys.to_h do |name|
|
|
533
|
+
connection = Faraday.new do |builder|
|
|
534
|
+
builder.response :raise_error
|
|
535
|
+
builder.adapter :test do |stub|
|
|
536
|
+
stub.delete("/api/rest/v1/bundles/7") do |env|
|
|
537
|
+
requests[name] << [ env.method, env.url.path, env.url.query, env.request_headers["X-FilesAPI-Key"] ]
|
|
538
|
+
[ 200, { "Content-Type" => "application/json" }, "{}" ]
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
[ name, described_class.new(connection) ]
|
|
543
|
+
end
|
|
544
|
+
attributes = { id: 7 }
|
|
545
|
+
constructor_options = {}.freeze
|
|
546
|
+
params = {}.freeze
|
|
547
|
+
bundle = Files::Bundle.new(attributes, constructor_options)
|
|
548
|
+
|
|
549
|
+
Files.api_key = "first-key"
|
|
550
|
+
clients[:a].request { bundle.delete(params) }
|
|
551
|
+
Files.api_key = "second-key"
|
|
552
|
+
clients[:b].request { bundle.delete(params) }
|
|
553
|
+
|
|
554
|
+
expect(requests[:a]).to eq([ [ :delete, "/api/rest/v1/bundles/7", "id=7", "first-key" ] ] * 2)
|
|
555
|
+
expect(requests[:b]).to be_empty
|
|
556
|
+
expect(bundle.options).to include(client: clients[:a], api_key: "first-key")
|
|
557
|
+
expect(bundle.attributes).to equal(attributes)
|
|
558
|
+
expect([ constructor_options, params, attributes ]).to eq([ {}, {}, { id: 7 } ])
|
|
559
|
+
expect(Files.api_key).to eq("second-key")
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
it "refuses a model update whose object parameter is not a Hash before sending it, leaving the model unchanged" do
|
|
563
|
+
dispatches = 0
|
|
564
|
+
client = stubbed_api_client do |stub|
|
|
565
|
+
stub.patch("/api/rest/v1/bundles/837") do
|
|
566
|
+
dispatches += 1
|
|
567
|
+
json_response(200, { id: 837 })
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
attributes = { id: 837 }
|
|
571
|
+
options = { api_key: "key", client: client }.freeze
|
|
572
|
+
params = { watermark_value: "wrong-object-type837" }.freeze
|
|
573
|
+
bundle = Files::Bundle.new(attributes, options)
|
|
574
|
+
|
|
575
|
+
expect { bundle.update(params) }.to raise_error(Files::InvalidParameterError, /watermark_value/) { |error|
|
|
576
|
+
expect(error).to be_a(Files::Error)
|
|
577
|
+
}
|
|
578
|
+
expect(dispatches).to eq(0)
|
|
579
|
+
expect(bundle.attributes).to equal(attributes)
|
|
580
|
+
expect(bundle.options).to equal(options)
|
|
581
|
+
expect([ attributes, params ]).to eq([ { id: 837 }, { watermark_value: "wrong-object-type837" } ])
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
describe "User-Agent" do
|
|
586
|
+
it "reports the SDK version the package declares" do
|
|
587
|
+
package_version = Gem::Specification.load(File.expand_path("../../files.com.gemspec", __dir__)).version.to_s
|
|
588
|
+
user_agent = described_class.new.send(:request_headers, nil, nil, :get, nil)["User-Agent"]
|
|
589
|
+
|
|
590
|
+
expect(user_agent).to match(/\AFiles\.com Ruby SDK v#{Regexp.escape(package_version)}(?: |\z)/)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
describe "retries" do
|
|
595
|
+
around do |example|
|
|
596
|
+
original = [ Files.max_network_retries, Files.initial_network_retry_delay, Files.max_network_retry_delay ]
|
|
597
|
+
Files.max_network_retries = 3
|
|
598
|
+
Files.initial_network_retry_delay = 0
|
|
599
|
+
Files.max_network_retry_delay = 0
|
|
600
|
+
example.run
|
|
601
|
+
ensure
|
|
602
|
+
Files.max_network_retries, Files.initial_network_retry_delay, Files.max_network_retry_delay = original
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
it "requests again after HTTP 500, but sends HTTP 501 Not Implemented requests once" do
|
|
606
|
+
dispatches = Hash.new(0)
|
|
607
|
+
client = stubbed_api_client do |stub|
|
|
608
|
+
[ 500, 501 ].each do |status|
|
|
609
|
+
stub.get("/api/rest/v1/workspaces/#{status}") do
|
|
610
|
+
dispatches[status] += 1
|
|
611
|
+
[ status, { "Content-Type" => "application/json" }, { error: "HTTP #{status}" }.to_json ]
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
[ 500, 501 ].each do |status|
|
|
617
|
+
expect { Files::Workspace.find(status, {}, api_key: "key", client: client) }.to raise_error(Files::APIError)
|
|
618
|
+
end
|
|
619
|
+
expect(dispatches).to eq(500 => 4, 501 => 1)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
it "requests again after HTTP 429 Too Many Requests and returns the record that follows" do
|
|
623
|
+
statuses = [ 429, 429, 200 ]
|
|
624
|
+
dispatches = 0
|
|
625
|
+
client = stubbed_api_client do |stub|
|
|
626
|
+
stub.get("/api/rest/v1/workspaces/7") do
|
|
627
|
+
dispatches += 1
|
|
628
|
+
next json_response(200, { id: 7, name: "Primary" }) if statuses.shift == 200
|
|
629
|
+
|
|
630
|
+
json_response(429, { error: "Slow down", type: "too-many-requests" })
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
workspace = Files::Workspace.find(7, {}, api_key: "key", client: client)
|
|
635
|
+
|
|
636
|
+
expect(dispatches).to eq(3)
|
|
637
|
+
expect(workspace).to be_a(Files::Workspace)
|
|
638
|
+
expect([ workspace.id, workspace.attributes[:name] ]).to eq([ 7, "Primary" ])
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
444
642
|
describe "network timeouts" do
|
|
445
643
|
around do |example|
|
|
446
644
|
original = [ Files.open_timeout, Files.read_timeout ]
|
|
@@ -458,4 +656,298 @@ RSpec.describe Files::ApiClient do
|
|
|
458
656
|
expect([ Files.open_timeout, Files.read_timeout ]).to eq([ 30, 60 ])
|
|
459
657
|
end
|
|
460
658
|
end
|
|
659
|
+
|
|
660
|
+
describe "base URLs" do
|
|
661
|
+
around do |example|
|
|
662
|
+
original = Files.base_url
|
|
663
|
+
example.run
|
|
664
|
+
ensure
|
|
665
|
+
Files.base_url = original
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
it "refuses a base URL scheme other than http or https before sending the request" do
|
|
669
|
+
dispatches = 0
|
|
670
|
+
client = stubbed_api_client do |stub|
|
|
671
|
+
stub.get("/api/rest/v1/bundles/83") do
|
|
672
|
+
dispatches += 1
|
|
673
|
+
json_response(200, { id: 83 })
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
Files.base_url = "unsupported://127.0.0.1:60548"
|
|
677
|
+
|
|
678
|
+
expect { Files::Bundle.find(83, {}, api_key: "key", client: client) }.to raise_error(Files::InvalidParameterError) { |error|
|
|
679
|
+
expect(error).to be_a(Files::Error)
|
|
680
|
+
expect(error.message).to include('"unsupported"', "https://")
|
|
681
|
+
}
|
|
682
|
+
expect(dispatches).to eq(0)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
it "refuses a base URL with an invalid port before sending the request, keeping the parser's error as its cause" do
|
|
686
|
+
dispatches = 0
|
|
687
|
+
client = stubbed_api_client do |stub|
|
|
688
|
+
stub.get("/api/rest/v1/bundles/83") do
|
|
689
|
+
dispatches += 1
|
|
690
|
+
json_response(200, { id: 83 })
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
Files.base_url = "http://127.0.0.1:63121invalid-port"
|
|
694
|
+
|
|
695
|
+
expect { Files::Bundle.find(83, {}, api_key: "key", client: client) }.to raise_error(Files::InvalidParameterError) { |error|
|
|
696
|
+
expect(error).to be_a(Files::Error)
|
|
697
|
+
expect(error.message).to include("port", "https://")
|
|
698
|
+
expect(error.message).not_to include("127.0.0.1")
|
|
699
|
+
expect(error.cause).to be_a(Addressable::URI::InvalidURIError)
|
|
700
|
+
}
|
|
701
|
+
expect(dispatches).to eq(0)
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
it "reports a refused connection to the base URL as an APIConnectionError caused by the network error" do
|
|
705
|
+
original_retries = Files.max_network_retries
|
|
706
|
+
Files.max_network_retries = 0
|
|
707
|
+
closed_server = TCPServer.new("127.0.0.1", 0)
|
|
708
|
+
port = closed_server.addr[1]
|
|
709
|
+
closed_server.close
|
|
710
|
+
Files.base_url = "http://127.0.0.1:#{port}"
|
|
711
|
+
|
|
712
|
+
expect { Files::Bundle.find(832, {}, api_key: "key", client: described_class.new) }.to raise_error(Files::APIConnectionError) { |error|
|
|
713
|
+
expect(error).to be_a(Files::Error)
|
|
714
|
+
expect(error.message).to include("http://127.0.0.1:#{port}")
|
|
715
|
+
causes = Enumerator.produce(error.cause, &:cause).take_while(&:itself)
|
|
716
|
+
expect(causes.first).to be_a(Faraday::ConnectionFailed)
|
|
717
|
+
expect(causes.map(&:class)).to include(Errno::ECONNREFUSED)
|
|
718
|
+
}
|
|
719
|
+
ensure
|
|
720
|
+
Files.max_network_retries = original_retries
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
it "reports a base URL whose certificate fails verification as an APIConnectionError caused by the TLS error" do
|
|
724
|
+
original_retries = Files.max_network_retries
|
|
725
|
+
Files.max_network_retries = 1
|
|
726
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
727
|
+
certificate = OpenSSL::X509::Certificate.new
|
|
728
|
+
certificate.version = 2
|
|
729
|
+
certificate.serial = 1
|
|
730
|
+
certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse("/CN=127.0.0.1")
|
|
731
|
+
certificate.public_key = key.public_key
|
|
732
|
+
certificate.not_before = Time.now - 60
|
|
733
|
+
certificate.not_after = Time.now + 3600
|
|
734
|
+
certificate.sign(key, OpenSSL::Digest.new("SHA256"))
|
|
735
|
+
tls = OpenSSL::SSL::SSLContext.new
|
|
736
|
+
tls.cert = certificate
|
|
737
|
+
tls.key = key
|
|
738
|
+
listener = TCPServer.new("127.0.0.1", 0)
|
|
739
|
+
server = OpenSSL::SSL::SSLServer.new(listener, tls)
|
|
740
|
+
peer = Thread.new do
|
|
741
|
+
loop do
|
|
742
|
+
server.accept.close
|
|
743
|
+
rescue OpenSSL::SSL::SSLError, SystemCallError
|
|
744
|
+
next
|
|
745
|
+
end
|
|
746
|
+
rescue IOError
|
|
747
|
+
nil
|
|
748
|
+
end
|
|
749
|
+
base_url = "https://127.0.0.1:#{listener.addr[1]}"
|
|
750
|
+
Files.base_url = base_url
|
|
751
|
+
|
|
752
|
+
expect { Files::Bundle.find(839, {}, api_key: "key", client: described_class.new) }.to raise_error(Files::APIConnectionError, /certificate verify failed/) { |error|
|
|
753
|
+
expect(error).to be_a(Files::Error)
|
|
754
|
+
expect(error.message).to include(base_url)
|
|
755
|
+
causes = Enumerator.produce(error.cause, &:cause).take_while(&:itself)
|
|
756
|
+
expect(causes.first).to be_a(Faraday::SSLError)
|
|
757
|
+
expect(causes.map(&:class)).to include(OpenSSL::SSL::SSLError)
|
|
758
|
+
}
|
|
759
|
+
ensure
|
|
760
|
+
listener&.close
|
|
761
|
+
peer&.kill
|
|
762
|
+
peer&.join
|
|
763
|
+
Files.max_network_retries = original_retries
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
it "continues a request once at the regional origin a lockout region mismatch names, and keeps that origin for the returned model" do
|
|
767
|
+
certificate = lambda do |subject, key, issuer, issuer_key, extensions|
|
|
768
|
+
cert = OpenSSL::X509::Certificate.new
|
|
769
|
+
cert.version = 2
|
|
770
|
+
cert.serial = issuer ? 2 : 1
|
|
771
|
+
cert.subject = OpenSSL::X509::Name.parse(subject)
|
|
772
|
+
cert.issuer = issuer ? issuer.subject : cert.subject
|
|
773
|
+
cert.public_key = key.public_key
|
|
774
|
+
cert.not_before = Time.now - 60
|
|
775
|
+
cert.not_after = Time.now + 3600
|
|
776
|
+
factory = OpenSSL::X509::ExtensionFactory.new(issuer || cert, cert)
|
|
777
|
+
extensions.each { |name, value, critical| cert.add_extension(factory.create_extension(name, value, critical)) }
|
|
778
|
+
cert.sign(issuer_key, OpenSSL::Digest.new("SHA256"))
|
|
779
|
+
end
|
|
780
|
+
ca_key = OpenSSL::PKey::RSA.new(2048)
|
|
781
|
+
ca_extensions = [ [ "basicConstraints", "CA:TRUE", true ], [ "keyUsage", "keyCertSign, cRLSign", true ], [ "subjectKeyIdentifier", "hash", false ] ]
|
|
782
|
+
ca = certificate.call("/CN=Files SDK region test CA", ca_key, nil, ca_key, ca_extensions)
|
|
783
|
+
peer_key = OpenSSL::PKey::RSA.new(2048)
|
|
784
|
+
peer_extensions = [
|
|
785
|
+
[ "basicConstraints", "CA:FALSE", true ], [ "keyUsage", "digitalSignature, keyEncipherment", true ], [ "extendedKeyUsage", "serverAuth", false ],
|
|
786
|
+
[ "subjectAltName", "DNS:issuer.example.com, DNS:target.example.com", false ], [ "authorityKeyIdentifier", "keyid", false ]
|
|
787
|
+
]
|
|
788
|
+
tls = OpenSSL::SSL::SSLContext.new
|
|
789
|
+
tls.key = peer_key
|
|
790
|
+
tls.cert = certificate.call("/CN=issuer.example.com", peer_key, ca, ca_key, peer_extensions)
|
|
791
|
+
trust = OpenSSL::X509::Store.new
|
|
792
|
+
trust.add_cert(ca)
|
|
793
|
+
# The fixture connection's proxy is this peer. It accepts the two CONNECTs the request needs and serves both
|
|
794
|
+
# origins over TLS itself: the configured issuer answers with the region mismatch, and the regional origin it
|
|
795
|
+
# names answers normally. Readiness waits are bounded at 10 seconds; reads and handshakes also rely on the
|
|
796
|
+
# client's timeouts. Cleanup below closes every accepted socket and checks that the owned peer finishes.
|
|
797
|
+
requests = []
|
|
798
|
+
accepted = []
|
|
799
|
+
listener = TCPServer.new("127.0.0.1", 0)
|
|
800
|
+
require "io/wait"
|
|
801
|
+
wait_readable = lambda do |io|
|
|
802
|
+
decrypted = io.is_a?(OpenSSL::SSL::SSLSocket) && io.pending.positive?
|
|
803
|
+
raise IOError, "the region fixture peer waited too long" unless decrypted || io.to_io.wait_readable(10)
|
|
804
|
+
end
|
|
805
|
+
peer = Thread.new do
|
|
806
|
+
2.times do
|
|
807
|
+
wait_readable.call(listener)
|
|
808
|
+
socket = listener.accept
|
|
809
|
+
accepted << socket
|
|
810
|
+
wait_readable.call(socket)
|
|
811
|
+
authority = socket.gets.to_s.split[1]
|
|
812
|
+
nil until socket.gets.to_s.chomp.empty?
|
|
813
|
+
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n")
|
|
814
|
+
wait_readable.call(socket)
|
|
815
|
+
tls_socket = OpenSSL::SSL::SSLSocket.new(socket, tls)
|
|
816
|
+
tls_socket.accept
|
|
817
|
+
wait_readable.call(tls_socket)
|
|
818
|
+
request_line = tls_socket.gets.to_s.split
|
|
819
|
+
headers = {}
|
|
820
|
+
while (line = tls_socket.gets.to_s.chomp) != ""
|
|
821
|
+
name, value = line.split(":", 2)
|
|
822
|
+
headers[name.downcase] = value.to_s.strip
|
|
823
|
+
end
|
|
824
|
+
requests << [ authority, request_line[0], request_line[1], headers["x-filesapi-key"] ]
|
|
825
|
+
status, body = if authority == "issuer.example.com:443"
|
|
826
|
+
[ "403 Forbidden", { error: "Lockout region mismatch", type: "not-authenticated/lockout-region-mismatch", data: { host: "target.example.com" } } ]
|
|
827
|
+
else
|
|
828
|
+
[ "200 OK", { id: 840, code: "regional" } ]
|
|
829
|
+
end
|
|
830
|
+
json = body.to_json
|
|
831
|
+
tls_socket.write("HTTP/1.1 #{status}\r\nContent-Type: application/json\r\nContent-Length: #{json.bytesize}\r\nConnection: close\r\n\r\n#{json}")
|
|
832
|
+
tls_socket.close
|
|
833
|
+
rescue OpenSSL::SSL::SSLError, IOError, SystemCallError
|
|
834
|
+
next
|
|
835
|
+
ensure
|
|
836
|
+
socket&.close
|
|
837
|
+
end
|
|
838
|
+
end
|
|
839
|
+
# A connection this example owns, trusting only its own certificate authority and using the peer as its proxy.
|
|
840
|
+
connection = described_class.build_default_conn
|
|
841
|
+
connection.proxy = "http://127.0.0.1:#{listener.addr[1]}"
|
|
842
|
+
connection.ssl.cert_store = trust
|
|
843
|
+
Files.base_url = "https://issuer.example.com"
|
|
844
|
+
|
|
845
|
+
bundle = Files::Bundle.find(840, {}, api_key: "key", client: described_class.new(connection))
|
|
846
|
+
|
|
847
|
+
expect([ bundle.id, bundle.attributes[:code] ]).to eq([ 840, "regional" ])
|
|
848
|
+
expect(bundle.options).to include(base_url: "https://target.example.com")
|
|
849
|
+
path = "/api/rest/v1/bundles/840?id=840"
|
|
850
|
+
expect(requests).to eq([ [ "issuer.example.com:443", "GET", path, "key" ], [ "target.example.com:443", "GET", path, "key" ] ])
|
|
851
|
+
expect(Files.base_url).to eq("https://issuer.example.com")
|
|
852
|
+
expect(peer.join(10)).to equal(peer)
|
|
853
|
+
ensure
|
|
854
|
+
connection&.close
|
|
855
|
+
listener&.close
|
|
856
|
+
accepted&.each(&:close)
|
|
857
|
+
peer&.join(10)
|
|
858
|
+
raise "the region fixture peer did not finish" if peer&.alive?
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
it "continues a request once over plain HTTP within a library-wide development origin pair, asking the region handler, and keeps that origin and pair for the returned model" do
|
|
862
|
+
original_base_url = Files.base_url
|
|
863
|
+
original_pairs = Files.region_http_pairs
|
|
864
|
+
original_handler = Files.region_handler
|
|
865
|
+
# Two plain HTTP origins this example owns, each a listener on its own port. The issuer answers with the region
|
|
866
|
+
# mismatch, advertising the other origin as a bare host and port, and that one answers normally. Readiness waits
|
|
867
|
+
# are bounded at 10 seconds; the rest of each request is read relying on the client's timeouts. Cleanup below
|
|
868
|
+
# closes every listener and accepted socket and checks that the owned peer finishes.
|
|
869
|
+
requests = []
|
|
870
|
+
accepted = []
|
|
871
|
+
events = []
|
|
872
|
+
issuer_listener = TCPServer.new("127.0.0.1", 0)
|
|
873
|
+
target_listener = TCPServer.new("127.0.0.1", 0)
|
|
874
|
+
issuer = "http://127.0.0.1:#{issuer_listener.addr[1]}"
|
|
875
|
+
target_host = "127.0.0.1:#{target_listener.addr[1]}"
|
|
876
|
+
target = "http://#{target_host}"
|
|
877
|
+
require "io/wait"
|
|
878
|
+
wait_readable = lambda do |io|
|
|
879
|
+
raise IOError, "the HTTP region fixture peer waited too long" unless io.wait_readable(10)
|
|
880
|
+
end
|
|
881
|
+
answers = [
|
|
882
|
+
[ issuer, issuer_listener, "403 Forbidden", { error: "Lockout region mismatch", type: "not-authenticated/lockout-region-mismatch", data: { host: target_host } } ],
|
|
883
|
+
[ target, target_listener, "200 OK", { id: 841, code: "regional" } ]
|
|
884
|
+
]
|
|
885
|
+
peer = Thread.new do
|
|
886
|
+
answers.each do |origin, listener, status, body|
|
|
887
|
+
wait_readable.call(listener)
|
|
888
|
+
socket = listener.accept
|
|
889
|
+
accepted << socket
|
|
890
|
+
wait_readable.call(socket)
|
|
891
|
+
request_line = socket.gets.to_s.split
|
|
892
|
+
headers = {}
|
|
893
|
+
while (line = socket.gets.to_s.chomp) != ""
|
|
894
|
+
name, value = line.split(":", 2)
|
|
895
|
+
headers[name.downcase] = value.to_s.strip
|
|
896
|
+
end
|
|
897
|
+
requests << [ origin, request_line[0], request_line[1], headers["x-filesapi-key"] ]
|
|
898
|
+
json = body.to_json
|
|
899
|
+
socket.write("HTTP/1.1 #{status}\r\nContent-Type: application/json\r\nContent-Length: #{json.bytesize}\r\nConnection: close\r\n\r\n#{json}")
|
|
900
|
+
rescue IOError, SystemCallError
|
|
901
|
+
next
|
|
902
|
+
ensure
|
|
903
|
+
socket&.close
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
# The library-wide pair, set from the caller's own Arrays and Strings; a region handler that records its event and
|
|
907
|
+
# approves the move by returning its own mutable copy of the target origin; and a connection and client this
|
|
908
|
+
# example owns, which use the library-wide pairs.
|
|
909
|
+
pairs = [ [ issuer.dup, target.dup ] ]
|
|
910
|
+
Files.region_http_pairs = pairs
|
|
911
|
+
approved_target = target.dup
|
|
912
|
+
Files.region_handler = lambda do |event|
|
|
913
|
+
events << event
|
|
914
|
+
approved_target
|
|
915
|
+
end
|
|
916
|
+
connection = described_class.build_default_conn
|
|
917
|
+
client = described_class.new(connection)
|
|
918
|
+
Files.base_url = issuer
|
|
919
|
+
|
|
920
|
+
bundle = Files::Bundle.find(841, {}, api_key: "key", client: client)
|
|
921
|
+
|
|
922
|
+
expect([ bundle.id, bundle.attributes[:code] ]).to eq([ 841, "regional" ])
|
|
923
|
+
expect(bundle.options).to include(base_url: target, region_http_pairs: [ [ issuer, target ] ])
|
|
924
|
+
expect(bundle.options[:client]).to equal(client)
|
|
925
|
+
path = "/api/rest/v1/bundles/841?id=841"
|
|
926
|
+
expect(requests).to eq([ [ issuer, "GET", path, "key" ], [ target, "GET", path, "key" ] ])
|
|
927
|
+
expect(events).to eq([ { reason: :lockout_region_mismatch, original_origin: issuer, target_origin: target, approval_required: false } ])
|
|
928
|
+
expect(events).to all(be_frozen)
|
|
929
|
+
expect(Files.base_url).to eq(issuer)
|
|
930
|
+
expect(peer.join(10)).to equal(peer)
|
|
931
|
+
# The returned model keeps the origin and pairs its request used: changing the handler's String, the caller's
|
|
932
|
+
# Arrays and Strings, or replacing the library-wide pairs, leaves them as they were, while the client's new
|
|
933
|
+
# requests start from the new setting.
|
|
934
|
+
approved_target << "/changed"
|
|
935
|
+
pairs.first.first << "/changed"
|
|
936
|
+
pairs << [ target, issuer ]
|
|
937
|
+
Files.region_http_pairs = []
|
|
938
|
+
expect(bundle.options[:base_url]).to eq(target)
|
|
939
|
+
expect(bundle.options[:region_http_pairs]).to eq([ [ issuer, target ] ]).and be_frozen
|
|
940
|
+
expect(client.region_http_pairs).to eq([])
|
|
941
|
+
ensure
|
|
942
|
+
connection&.close
|
|
943
|
+
issuer_listener&.close
|
|
944
|
+
target_listener&.close
|
|
945
|
+
accepted&.each(&:close)
|
|
946
|
+
peer&.join(10)
|
|
947
|
+
Files.base_url = original_base_url
|
|
948
|
+
Files.region_http_pairs = original_pairs
|
|
949
|
+
Files.region_handler = original_handler
|
|
950
|
+
raise "the HTTP region fixture peer did not finish" if peer&.alive?
|
|
951
|
+
end
|
|
952
|
+
end
|
|
461
953
|
end
|
data/spec/list_spec.rb
CHANGED
|
@@ -75,6 +75,31 @@ RSpec.describe Files::List do
|
|
|
75
75
|
expect(seen).to eq([ 1, 2 ])
|
|
76
76
|
expect(requested_cursors).to eq([ nil, "a" ])
|
|
77
77
|
end
|
|
78
|
+
|
|
79
|
+
it "follows X-Files-Cursor-Next, or X-Files-Cursor without it, and raises a later page's error after each record once" do
|
|
80
|
+
pages = {
|
|
81
|
+
nil => [ [ 1 ], { "X-Files-Cursor-Next" => "2:a+b&=opaque" } ],
|
|
82
|
+
"2:a+b&=opaque" => [ [ 2 ], { "X-Files-Cursor" => "legacy-3" } ],
|
|
83
|
+
"legacy-3" => [ [ 3 ], { "X-Files-Cursor-Next" => "next-4", "X-Files-Cursor" => "stale" } ],
|
|
84
|
+
}
|
|
85
|
+
client = stubbed_api_client do |stub|
|
|
86
|
+
stub.get("/api/rest/v1/workspaces") do |env|
|
|
87
|
+
requests << env.params
|
|
88
|
+
next json_response(404, { error: "Not Found", type: "not-found" }) if env.params["cursor"] == "next-4"
|
|
89
|
+
|
|
90
|
+
ids, headers = pages.fetch(env.params["cursor"])
|
|
91
|
+
json_response(200, ids.map { |id| { id: id } }, headers)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
seen = []
|
|
95
|
+
|
|
96
|
+
expect {
|
|
97
|
+
Files::Workspace.list({ per_page: 1, filter: { name: "Workspace" } }, api_key: "list-key", client: client).auto_paging_each { |workspace| seen << workspace.id }
|
|
98
|
+
}.to raise_error(Files::NotFoundError)
|
|
99
|
+
expect(seen).to eq([ 1, 2, 3 ])
|
|
100
|
+
expect(requested_cursors).to eq([ nil, "2:a+b&=opaque", "legacy-3", "next-4" ])
|
|
101
|
+
expect(requests).to all(include("per_page" => "1", "filter" => { "name" => "Workspace" }))
|
|
102
|
+
end
|
|
78
103
|
end
|
|
79
104
|
|
|
80
105
|
describe "#each" do
|
|
@@ -132,6 +157,8 @@ RSpec.describe Files::List do
|
|
|
132
157
|
end
|
|
133
158
|
|
|
134
159
|
it "requests the same page again after a failed request" do
|
|
160
|
+
original_retries = Files.max_network_retries
|
|
161
|
+
Files.max_network_retries = 0
|
|
135
162
|
failures = [ json_response(429, { error: "Slow down", type: "too-many-requests" }) ]
|
|
136
163
|
client = stubbed_api_client do |stub|
|
|
137
164
|
stub.get("/api/rest/v1/workspaces") do |env|
|
|
@@ -147,6 +174,8 @@ RSpec.describe Files::List do
|
|
|
147
174
|
expect { page.next_page }.to raise_error(Files::TooManyRequestsError)
|
|
148
175
|
expect(page.next_page.map(&:id)).to eq([ 2 ])
|
|
149
176
|
expect(requested_cursors).to eq([ nil, "c2", "c2" ])
|
|
177
|
+
ensure
|
|
178
|
+
Files.max_network_retries = original_retries
|
|
150
179
|
end
|
|
151
180
|
|
|
152
181
|
{
|
|
@@ -200,7 +229,7 @@ RSpec.describe Files::List do
|
|
|
200
229
|
json_response(200, [])
|
|
201
230
|
end
|
|
202
231
|
end
|
|
203
|
-
options = { api_key: "list-key" }
|
|
232
|
+
options = { api_key: "list-key" }.freeze
|
|
204
233
|
list = Files::Workspace.list({}, options)
|
|
205
234
|
|
|
206
235
|
expect { rejecting.request { list.to_a } }.to raise_error(Files::APIConnectionError)
|
|
@@ -209,7 +238,7 @@ RSpec.describe Files::List do
|
|
|
209
238
|
first_ids = nil
|
|
210
239
|
accepting.request { first_ids = list.map(&:id) }
|
|
211
240
|
expect(first_ids).to eq([ 1 ])
|
|
212
|
-
expect(options).to
|
|
241
|
+
expect(options).to eq(api_key: "list-key")
|
|
213
242
|
|
|
214
243
|
second = nil
|
|
215
244
|
other.request { second = list.next_page }
|
|
@@ -120,7 +120,7 @@ RSpec.describe "Saving and updating models" do
|
|
|
120
120
|
expect(Files::File.zip_list_contents("empty.zip", {}, options)).to eq([])
|
|
121
121
|
expect(Files::File.begin_upload("empty-part.csv", {}, options).map { |part| [ part.class, part.attributes ] }).to eq([ [ Files::FileUploadPart, {} ] ])
|
|
122
122
|
end
|
|
123
|
-
expect(options).to
|
|
123
|
+
expect(options).to eq(api_key: "key-a")
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
it "refreshes each model from its own client, keeping false, zero, empty, null and decimal values" do
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require_relative "../support/stubbed_api"
|
|
3
|
+
|
|
4
|
+
RSpec.describe Files::User do
|
|
5
|
+
include StubbedApi
|
|
6
|
+
|
|
7
|
+
it "rejects an Array given as the Boolean disabled before sending the update" do
|
|
8
|
+
requests = []
|
|
9
|
+
client = stubbed_api_client do |stub|
|
|
10
|
+
stub.patch("/api/rest/v1/users/842") do |env|
|
|
11
|
+
requests << env
|
|
12
|
+
json_response(200, { id: 842 })
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
user = described_class.new({ id: 842 }, api_key: "key", client: client)
|
|
16
|
+
|
|
17
|
+
expect { user.update(disabled: [ "wrong-Boolean-container842" ]) }.to raise_error(Files::InvalidParameterError, /disabled/)
|
|
18
|
+
expect(requests).to be_empty
|
|
19
|
+
end
|
|
20
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: files.com
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.787
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- files.com
|
|
@@ -493,6 +493,7 @@ files:
|
|
|
493
493
|
- spec/models/file_underscore_destinations_spec.rb
|
|
494
494
|
- spec/models/folder_spec.rb
|
|
495
495
|
- spec/models/persistence_spec.rb
|
|
496
|
+
- spec/models/user_spec.rb
|
|
496
497
|
- spec/path_util_spec.rb
|
|
497
498
|
- spec/spec_helper.rb
|
|
498
499
|
- spec/support/stubbed_api.rb
|