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.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/_VERSION +1 -1
  4. data/lib/files.com/api.rb +20 -5
  5. data/lib/files.com/api_client.rb +196 -17
  6. data/lib/files.com/list.rb +4 -2
  7. data/lib/files.com/models/action_log.rb +4 -1
  8. data/lib/files.com/models/action_notification_export.rb +3 -1
  9. data/lib/files.com/models/action_notification_export_result.rb +4 -1
  10. data/lib/files.com/models/ai_assistant_personality.rb +20 -7
  11. data/lib/files.com/models/ai_task.rb +22 -10
  12. data/lib/files.com/models/api_key.rb +15 -7
  13. data/lib/files.com/models/api_request_log.rb +4 -1
  14. data/lib/files.com/models/app.rb +4 -1
  15. data/lib/files.com/models/as2_incoming_message.rb +4 -1
  16. data/lib/files.com/models/as2_outgoing_message.rb +4 -1
  17. data/lib/files.com/models/as2_partner.rb +18 -7
  18. data/lib/files.com/models/as2_station.rb +14 -7
  19. data/lib/files.com/models/automation.rb +47 -13
  20. data/lib/files.com/models/automation_log.rb +4 -1
  21. data/lib/files.com/models/automation_run.rb +16 -9
  22. data/lib/files.com/models/bandwidth_snapshot.rb +4 -1
  23. data/lib/files.com/models/behavior.rb +27 -9
  24. data/lib/files.com/models/bundle.rb +57 -10
  25. data/lib/files.com/models/bundle_action.rb +4 -1
  26. data/lib/files.com/models/bundle_download.rb +4 -1
  27. data/lib/files.com/models/bundle_notification.rb +20 -7
  28. data/lib/files.com/models/bundle_recipient.rb +6 -1
  29. data/lib/files.com/models/bundle_registration.rb +4 -1
  30. data/lib/files.com/models/chat_session.rb +5 -2
  31. data/lib/files.com/models/child_site_management_policy.rb +18 -7
  32. data/lib/files.com/models/clickwrap.rb +14 -7
  33. data/lib/files.com/models/custom_domain.rb +18 -8
  34. data/lib/files.com/models/desktop_configuration_profile.rb +21 -7
  35. data/lib/files.com/models/dns_record.rb +4 -1
  36. data/lib/files.com/models/email_incoming_message.rb +4 -1
  37. data/lib/files.com/models/email_log.rb +4 -1
  38. data/lib/files.com/models/event_channel.rb +20 -7
  39. data/lib/files.com/models/event_delivery_attempt.rb +5 -2
  40. data/lib/files.com/models/event_record.rb +5 -2
  41. data/lib/files.com/models/event_subscription.rb +25 -7
  42. data/lib/files.com/models/event_target.rb +22 -7
  43. data/lib/files.com/models/exavault_api_request_log.rb +4 -1
  44. data/lib/files.com/models/expectation.rb +23 -10
  45. data/lib/files.com/models/expectation_evaluation.rb +5 -2
  46. data/lib/files.com/models/expectation_incident.rb +20 -11
  47. data/lib/files.com/models/external_event.rb +6 -2
  48. data/lib/files.com/models/file.rb +110 -37
  49. data/lib/files.com/models/file_comment.rb +14 -7
  50. data/lib/files.com/models/file_comment_reaction.rb +6 -3
  51. data/lib/files.com/models/file_migration.rb +1 -1
  52. data/lib/files.com/models/file_migration_log.rb +4 -1
  53. data/lib/files.com/models/folder.rb +11 -3
  54. data/lib/files.com/models/form_field_set.rb +26 -7
  55. data/lib/files.com/models/ftp_action_log.rb +4 -1
  56. data/lib/files.com/models/gpg_key.rb +15 -7
  57. data/lib/files.com/models/group.rb +30 -7
  58. data/lib/files.com/models/group_user.rb +16 -6
  59. data/lib/files.com/models/history.rb +23 -8
  60. data/lib/files.com/models/history_export.rb +2 -1
  61. data/lib/files.com/models/history_export_result.rb +4 -1
  62. data/lib/files.com/models/holiday_calendar.rb +15 -7
  63. data/lib/files.com/models/holiday_region.rb +4 -1
  64. data/lib/files.com/models/inbound_s3_log.rb +4 -1
  65. data/lib/files.com/models/inbox_recipient.rb +6 -1
  66. data/lib/files.com/models/inbox_registration.rb +4 -1
  67. data/lib/files.com/models/inbox_upload.rb +4 -1
  68. data/lib/files.com/models/integration_centric_profile.rb +17 -7
  69. data/lib/files.com/models/invoice.rb +5 -2
  70. data/lib/files.com/models/ip_address.rb +16 -4
  71. data/lib/files.com/models/key_lifecycle_rule.rb +17 -7
  72. data/lib/files.com/models/lock.rb +16 -6
  73. data/lib/files.com/models/metadata_category.rb +19 -9
  74. data/lib/files.com/models/notification.rb +39 -7
  75. data/lib/files.com/models/outbound_connection_log.rb +4 -1
  76. data/lib/files.com/models/partner.rb +32 -7
  77. data/lib/files.com/models/partner_channel.rb +17 -7
  78. data/lib/files.com/models/partner_channel_template.rb +17 -7
  79. data/lib/files.com/models/partner_site.rb +5 -3
  80. data/lib/files.com/models/partner_site_request.rb +10 -4
  81. data/lib/files.com/models/payment.rb +5 -2
  82. data/lib/files.com/models/pending_work_event.rb +5 -2
  83. data/lib/files.com/models/permission.rb +12 -4
  84. data/lib/files.com/models/public_hosting_request_log.rb +4 -1
  85. data/lib/files.com/models/public_key.rb +15 -7
  86. data/lib/files.com/models/remote_bandwidth_snapshot.rb +4 -1
  87. data/lib/files.com/models/remote_mount_backend.rb +25 -10
  88. data/lib/files.com/models/remote_server.rb +46 -14
  89. data/lib/files.com/models/remote_server_credential.rb +14 -7
  90. data/lib/files.com/models/request.rb +17 -6
  91. data/lib/files.com/models/restore.rb +8 -1
  92. data/lib/files.com/models/schedule.rb +14 -7
  93. data/lib/files.com/models/scheduled_export.rb +18 -7
  94. data/lib/files.com/models/scim_log.rb +5 -2
  95. data/lib/files.com/models/secret.rb +15 -7
  96. data/lib/files.com/models/session.rb +1 -0
  97. data/lib/files.com/models/settings_change.rb +4 -1
  98. data/lib/files.com/models/sftp_action_log.rb +4 -1
  99. data/lib/files.com/models/sftp_host_key.rb +17 -7
  100. data/lib/files.com/models/share_group.rb +14 -7
  101. data/lib/files.com/models/siem_http_destination.rb +67 -7
  102. data/lib/files.com/models/siem_http_destination_event.rb +5 -2
  103. data/lib/files.com/models/site.rb +109 -0
  104. data/lib/files.com/models/site_subdomain_redirect.rb +10 -5
  105. data/lib/files.com/models/snapshot.rb +19 -10
  106. data/lib/files.com/models/sso_event.rb +5 -2
  107. data/lib/files.com/models/sso_strategy.rb +10 -5
  108. data/lib/files.com/models/style.rb +9 -6
  109. data/lib/files.com/models/sync.rb +36 -13
  110. data/lib/files.com/models/sync_log.rb +4 -1
  111. data/lib/files.com/models/sync_run.rb +5 -2
  112. data/lib/files.com/models/usage_daily_snapshot.rb +4 -1
  113. data/lib/files.com/models/usage_snapshot.rb +4 -1
  114. data/lib/files.com/models/user.rb +124 -16
  115. data/lib/files.com/models/user_additional_email_recipient.rb +14 -7
  116. data/lib/files.com/models/user_cipher_use.rb +4 -1
  117. data/lib/files.com/models/user_lifecycle_rule.rb +26 -7
  118. data/lib/files.com/models/user_request.rb +11 -5
  119. data/lib/files.com/models/user_security_event.rb +5 -2
  120. data/lib/files.com/models/user_sftp_client_use.rb +4 -1
  121. data/lib/files.com/models/web_dav_action_log.rb +4 -1
  122. data/lib/files.com/models/webhook_test.rb +3 -0
  123. data/lib/files.com/models/workspace.rb +14 -7
  124. data/lib/files.com/util.rb +32 -1
  125. data/lib/files.com/version.rb +3 -1
  126. data/lib/files.com.rb +54 -1
  127. data/spec/lib/api_client_spec.rb +492 -0
  128. data/spec/list_spec.rb +31 -2
  129. data/spec/models/persistence_spec.rb +1 -1
  130. data/spec/models/user_spec.rb +20 -0
  131. metadata +2 -1
@@ -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 include(api_key: "list-key", client: accepting)
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 include(api_key: "key-a", client: client)
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.785
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