diff --git a/lib/privy/public_api/services/key_quorums.rb b/lib/privy/public_api/services/key_quorums.rb index 03e34c9..bd7d8c5 100644 --- a/lib/privy/public_api/services/key_quorums.rb +++ b/lib/privy/public_api/services/key_quorums.rb @@ -47,16 +47,20 @@ def create(key_quorum_create_params:, request_options: nil) # @option key_quorum_update_params [Array, nil] :user_ids User IDs to authorize. # @option key_quorum_update_params [Array, nil] :key_quorum_ids Nested key quorum IDs. # @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for signing. + # @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the + # request expires. Defaults to the value computed by the client's + # PrivyRequestExpiryOptions. # @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries). # # @return [Privy::Models::KeyQuorum] - def update(key_quorum_id, key_quorum_update_params:, authorization_context: nil, request_options: nil) + def update(key_quorum_id, key_quorum_update_params:, authorization_context: nil, request_expiry: nil, request_options: nil) prepared = Privy::Authorization.prepare_request( privy_client, method: :patch, url: Privy::Authorization.signed_url(privy_client, "v1/key_quorums/#{key_quorum_id}"), body: key_quorum_update_params, - authorization_context: authorization_context + authorization_context: authorization_context, + request_expiry: privy_client.compute_request_expiry(request_expiry) ) combined_params = key_quorum_update_params.merge(request_options: request_options) Privy::Authorization.merge_prepared_headers!(combined_params, prepared.headers) @@ -70,16 +74,20 @@ def update(key_quorum_id, key_quorum_update_params:, authorization_context: nil, # # @param key_quorum_id [String] ID of the key quorum to delete. # @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for signing. + # @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the + # request expires. Defaults to the value computed by the client's + # PrivyRequestExpiryOptions. # @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries). # # @return [Privy::Models::SuccessResponse] - def delete(key_quorum_id, authorization_context: nil, request_options: nil) + def delete(key_quorum_id, authorization_context: nil, request_expiry: nil, request_options: nil) prepared = Privy::Authorization.prepare_request( privy_client, method: :delete, url: Privy::Authorization.signed_url(privy_client, "v1/key_quorums/#{key_quorum_id}"), body: "", - authorization_context: authorization_context + authorization_context: authorization_context, + request_expiry: privy_client.compute_request_expiry(request_expiry) ) # Workaround: the Stainless-generated Ruby client sends Content-Type: application/json on # every request (lib/privy/internal/transport/base_client.rb:204), even bodyless DELETEs. diff --git a/test/privy/public_api/services/key_quorums_test.rb b/test/privy/public_api/services/key_quorums_test.rb new file mode 100644 index 0000000..d8658b2 --- /dev/null +++ b/test/privy/public_api/services/key_quorums_test.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require_relative "../../test_helper" + +class Privy::Services::KeyQuorumsTest < Minitest::Test + extend Minitest::Serial + include WebMock::API + + BASE_URL = "https://api.staging.privy.io" + + def before_all + super + WebMock.enable! + end + + def after_all + WebMock.disable! + super + end + + def teardown + WebMock.reset! + super + end + + def build_client(**opts) + Privy::PrivyClient.new(app_id: "app-abc", app_secret: "secret", environment: :staging, **opts) + end + + def key_quorum_response_body(**overrides) + { + id: "kq-1", + authorization_keys: [], + authorization_threshold: 1.0, + display_name: nil, + user_ids: nil + }.merge(overrides).to_json + end + + def stub_json(method, url, body:) + stub_request(method, url).to_return( + status: 200, + body: body, + headers: {"content-type" => "application/json"} + ) + end + + # --- request_expiry on update ---------------------------------------------- + + def test_update_default_request_expiry_is_auto_set + stub_json(:patch, "#{BASE_URL}/v1/key_quorums/kq-1", body: key_quorum_response_body) + before = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) + build_client.key_quorums.update("kq-1", key_quorum_update_params: {authorization_threshold: 1}) + assert_requested(:patch, "#{BASE_URL}/v1/key_quorums/kq-1") do |req| + assert_in_delta(before + (15 * 60 * 1_000), req.headers["Privy-Request-Expiry"].to_i, 5_000) + end + end + + def test_update_per_call_request_expiry_overrides_default + stub_json(:patch, "#{BASE_URL}/v1/key_quorums/kq-1", body: key_quorum_response_body) + build_client.key_quorums.update( + "kq-1", + key_quorum_update_params: {authorization_threshold: 1}, + request_expiry: 1_750_000_000_000 + ) + assert_requested(:patch, "#{BASE_URL}/v1/key_quorums/kq-1") do |req| + assert_equal("1750000000000", req.headers["Privy-Request-Expiry"]) + end + end + + def test_update_disabled_options_omit_header + stub_json(:patch, "#{BASE_URL}/v1/key_quorums/kq-1", body: key_quorum_response_body) + build_client(request_expiry: Privy::PrivyRequestExpiryOptions.build(disabled: true)) + .key_quorums.update("kq-1", key_quorum_update_params: {authorization_threshold: 1}) + assert_requested(:patch, "#{BASE_URL}/v1/key_quorums/kq-1") do |req| + refute(req.headers.key?("Privy-Request-Expiry")) + end + end + + def test_update_disabled_options_still_honor_explicit_override + stub_json(:patch, "#{BASE_URL}/v1/key_quorums/kq-1", body: key_quorum_response_body) + build_client(request_expiry: Privy::PrivyRequestExpiryOptions.build(disabled: true)) + .key_quorums.update( + "kq-1", + key_quorum_update_params: {authorization_threshold: 1}, + request_expiry: 1_750_000_000_000 + ) + assert_requested(:patch, "#{BASE_URL}/v1/key_quorums/kq-1") do |req| + assert_equal("1750000000000", req.headers["Privy-Request-Expiry"]) + end + end + + # --- request_expiry on delete ---------------------------------------------- + + def test_delete_per_call_request_expiry_overrides_default + stub_request(:delete, "#{BASE_URL}/v1/key_quorums/kq-1").to_return( + status: 200, body: '{"success":true}', headers: {"content-type" => "application/json"} + ) + build_client.key_quorums.delete("kq-1", request_expiry: 1_750_000_000_000) + assert_requested(:delete, "#{BASE_URL}/v1/key_quorums/kq-1") do |req| + assert_equal("1750000000000", req.headers["Privy-Request-Expiry"]) + end + end +end