From 7b7c22ed7d009451c8186c34cf02ff4fef277388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Tue, 3 Nov 2026 16:13:21 +0300 Subject: [PATCH 1/2] Fix dynamic REST property lookup --- CHANGELOG.md | 1 + lib/shopify_api/rest/base.rb | 32 ++++++++++++++----- test/rest/base_test.rb | 60 ++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 test/rest/base_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 619b7ee29..57acfd8f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api ## Unreleased +- [#1272](https://github.com/Shopify/shopify-api-ruby/issues/1272) and [#1273](https://github.com/Shopify/shopify-api-ruby/issues/1273) Raise `NoMethodError` for unknown REST property getters and report dynamic properties consistently from `respond_to?`. - [#1475](https://github.com/Shopify/shopify-api-ruby/pull/1475) Add an optional `client_assertion:` argument to `ShopifyAPI::Auth::ClientCredentials.client_credentials`. When supplied, the token request authenticates with a JWT client assertion and sends no client secret. Omitting it, or passing `nil`, keeps the existing client-secret behavior. See the [OAuth documentation](docs/usage/oauth.md#client-assertion-authentication). - [#1475](https://github.com/Shopify/shopify-api-ruby/pull/1475) Add `ShopifyAPI::Auth::ClientAssertion.create`, an optional helper that signs a client assertion with an `RS256` (RSA, 2048-bit minimum) or `ES256` (EC P-256) private key your app owns. `client_id` defaults to `ShopifyAPI::Context.api_key`, the only value the helper reads from the context, and it never generates, imports, stores, or rotates keys. - [#1475](https://github.com/Shopify/shopify-api-ruby/pull/1475) Add `ShopifyAPI::Errors::InvalidClientAssertionError`, raised for a blank or non-`String` assertion, for an assertion with surrounding whitespace, and for invalid signer input. diff --git a/lib/shopify_api/rest/base.rb b/lib/shopify_api/rest/base.rb index 3cc4ebd18..298ac7e76 100644 --- a/lib/shopify_api/rest/base.rb +++ b/lib/shopify_api/rest/base.rb @@ -11,6 +11,12 @@ class Base extend T::Helpers abstract! + INTERNAL_PROPERTIES = T.let( + %w[session client custom_prefix forced_nils aliased_properties].freeze, + T::Array[String], + ) + private_constant :INTERNAL_PROPERTIES + @headers = T.let(nil, T.nilable(T::Hash[T.any(Symbol, String), String])) @has_one = T.let({}, T::Hash[Symbol, T::Class[T.anything]]) @has_many = T.let({}, T::Hash[Symbol, T::Class[T.anything]]) @@ -185,7 +191,7 @@ def get_path(http_method:, operation:, entity: nil, ids: {}) url_ids = ids.transform_keys(&:to_sym) path_ids.each do |id| - if url_ids[id].nil? && (entity_id = entity&.public_send(id)) + if url_ids[id].nil? && (entity_id = entity&.send(:get_property, id)) url_ids[id] = entity_id end end @@ -315,16 +321,20 @@ def method_missing(meth_id, val = nil) set_property(var, val) @forced_nils[var] = val.nil? else + return super unless dynamic_property_defined?(var) + get_property(var) end end - sig { params(meth_id: Symbol, args: T.untyped).void } - def respond_to_missing?(meth_id, *args) + sig { params(meth_id: Symbol, include_private: T::Boolean).returns(T::Boolean) } + def respond_to_missing?(meth_id, include_private = false) str = meth_id.id2name match = str.match(/([^=]+)=/) - match.nil? ? true : super + return true unless match.nil? + + dynamic_property_defined?(str) || super(meth_id, include_private) end sig { params(saving: T::Boolean).returns(T::Hash[String, T.untyped]) } @@ -430,14 +440,14 @@ def attributes_to_update def build_required_attributes(http_method:) required_attributes = {} - primary_key_value = send(self.class.primary_key) + primary_key_value = get_property(self.class.primary_key) unless primary_key_value.nil? required_attributes[self.class.primary_key] = primary_key_value end path_ids = deduce_path_ids(http_method) path_ids&.each do |path_id| - path_id_value = send(path_id) + path_id_value = get_property(path_id) required_attributes[path_id.to_s] = path_id_value unless path_id_value.nil? end @@ -446,7 +456,7 @@ def build_required_attributes(http_method:) sig { returns(Symbol) } def deduce_write_verb - send(self.class.primary_key) ? :put : :post + get_property(self.class.primary_key) ? :put : :post end sig { params(method: Symbol).returns(T.nilable(String)) } @@ -488,7 +498,7 @@ def set_property(key, val) # variables. To work around that, we're cleaning them up here but keeping track of the properties that were # aliased this way. When loading up the property, we can map back from the "invalid" field so that it is # transparent to outside callers - clean_key = key.to_s.gsub(/[\?\s]/, "") + clean_key = key.to_s.gsub(/[?\s]/, "") @aliased_properties[key.to_s] = clean_key if clean_key != key instance_variable_set("@#{clean_key}", val) @@ -501,6 +511,12 @@ def get_property(key) instance_variable_get("@#{clean_key}") end + sig { params(key: String).returns(T::Boolean) } + def dynamic_property_defined?(key) + property = @aliased_properties[key] || key + !INTERNAL_PROPERTIES.include?(property) && instance_variable_defined?("@#{property}") + end + sig do params( element: T.nilable(T.any(T::Hash[String, T.untyped], ShopifyAPI::Rest::Base)), diff --git a/test/rest/base_test.rb b/test/rest/base_test.rb new file mode 100644 index 000000000..11ab3d2d5 --- /dev/null +++ b/test/rest/base_test.rb @@ -0,0 +1,60 @@ +# typed: strict +# frozen_string_literal: true + +require_relative "../test_helper" + +class RestBaseTest < Test::Unit::TestCase + def setup + super + + session = ShopifyAPI::Auth::Session.new(id: "id", shop: "test-shop.myshopify.io", access_token: "token") + attributes = { title: "T-shirt", published_at: nil } + attributes[:"tax_exempt?"] = true + @product = ShopifyAPI::Product.new( + session: session, + from_hash: attributes, + ) + end + + def test_missing_dynamic_getters_raise_and_are_not_reported_as_supported + assert_equal(false, @product.respond_to?(:missing_attribute)) + assert_raises(NoMethodError) { @product.missing_attribute } + assert_equal(false, @product.respond_to?(:missing_attribute)) + + assert_equal(false, @product.respond_to?(:session)) + assert_raises(NoMethodError) { @product.session } + end + + def test_respond_to_recognises_dynamic_properties_including_nil_and_aliased_values + assert_equal(true, @product.respond_to?(:title)) + assert_equal(true, @product.respond_to?(:published_at)) + assert_equal(true, @product.respond_to?(:tax_exempt?)) + end + + def test_dynamic_setters_remain_supported + assert_equal(true, @product.respond_to?(:custom_field=)) + + @product.custom_field = nil + + assert_equal(true, @product.respond_to?(:custom_field)) + assert_nil(@product.custom_field) + + @product.custom_field = "custom value" + + assert_equal(true, @product.respond_to?(:custom_field)) + assert_equal("custom value", @product.custom_field) + end + + def test_write_operations_treat_missing_resource_ids_as_nil + feedback = ShopifyAPI::ResourceFeedback.new( + session: ShopifyAPI::Auth::Session.new(id: "id", shop: "test-shop.myshopify.io", access_token: "token"), + ) + + assert_equal(:post, feedback.send(:deduce_write_verb)) + assert_equal({}, feedback.send(:build_required_attributes, http_method: :post)) + assert_equal( + "products.json", + ShopifyAPI::Product.get_path(http_method: :get, operation: :get, entity: feedback, ids: {}), + ) + end +end From 860f6c119eaa554299d7e7b47760a54ab73cf9b2 Mon Sep 17 00:00:00 2001 From: Efe Date: Wed, 4 Nov 2026 10:50:50 +0300 Subject: [PATCH 2/2] Preserve REST getter behavior during dynamic lookup Handle unsupported method names without leaking NameError and retain computed getters when reading optional resource identifiers. Keep internal client access private in test mocks. Correct dispute evidence description types and validate the default-address field without invoking its action method, so REST response checks remain non-vacuous. --- lib/shopify_api/rest/base.rb | 19 +++++-- .../resources/2022_07/dispute_evidence.rb | 4 +- .../resources/2022_10/dispute_evidence.rb | 4 +- .../resources/2023_01/dispute_evidence.rb | 4 +- .../resources/2023_04/dispute_evidence.rb | 4 +- .../resources/2023_07/dispute_evidence.rb | 4 +- .../resources/2023_10/dispute_evidence.rb | 4 +- .../resources/2024_01/dispute_evidence.rb | 4 +- .../resources/2024_04/dispute_evidence.rb | 4 +- .../resources/2024_07/dispute_evidence.rb | 4 +- .../resources/2024_10/dispute_evidence.rb | 4 +- .../resources/2025_01/dispute_evidence.rb | 4 +- .../resources/2025_04/dispute_evidence.rb | 4 +- .../resources/2025_07/dispute_evidence.rb | 4 +- .../resources/2025_10/dispute_evidence.rb | 4 +- .../resources/2026_01/dispute_evidence.rb | 4 +- .../resources/2026_04/dispute_evidence.rb | 4 +- .../resources/2026_07/dispute_evidence.rb | 4 +- test/clients/base_rest_resource_test.rb | 8 +-- test/rest/2022_04/customer_address_test.rb | 6 +++ test/rest/2022_07/customer_address_test.rb | 6 +++ test/rest/2022_10/customer_address_test.rb | 6 +++ test/rest/2023_01/customer_address_test.rb | 6 +++ test/rest/2023_04/customer_address_test.rb | 6 +++ test/rest/2023_07/customer_address_test.rb | 6 +++ test/rest/2023_10/customer_address_test.rb | 6 +++ test/rest/2024_01/customer_address_test.rb | 6 +++ test/rest/2024_04/customer_address_test.rb | 6 +++ test/rest/2024_07/customer_address_test.rb | 6 +++ test/rest/2024_10/customer_address_test.rb | 6 +++ test/rest/2025_01/customer_address_test.rb | 6 +++ test/rest/2025_04/customer_address_test.rb | 6 +++ test/rest/base_test.rb | 54 +++++++++++++++++++ 33 files changed, 184 insertions(+), 43 deletions(-) diff --git a/lib/shopify_api/rest/base.rb b/lib/shopify_api/rest/base.rb index 298ac7e76..ae5b084bf 100644 --- a/lib/shopify_api/rest/base.rb +++ b/lib/shopify_api/rest/base.rb @@ -12,7 +12,7 @@ class Base abstract! INTERNAL_PROPERTIES = T.let( - %w[session client custom_prefix forced_nils aliased_properties].freeze, + ["session", "client", "custom_prefix", "forced_nils", "aliased_properties"].freeze, T::Array[String], ) private_constant :INTERNAL_PROPERTIES @@ -191,7 +191,7 @@ def get_path(http_method:, operation:, entity: nil, ids: {}) url_ids = ids.transform_keys(&:to_sym) path_ids.each do |id| - if url_ids[id].nil? && (entity_id = entity&.send(:get_property, id)) + if url_ids[id].nil? && (entity_id = entity&.send(:get_optional_property, id)) url_ids[id] = entity_id end end @@ -440,14 +440,14 @@ def attributes_to_update def build_required_attributes(http_method:) required_attributes = {} - primary_key_value = get_property(self.class.primary_key) + primary_key_value = get_optional_property(self.class.primary_key, include_private: true) unless primary_key_value.nil? required_attributes[self.class.primary_key] = primary_key_value end path_ids = deduce_path_ids(http_method) path_ids&.each do |path_id| - path_id_value = get_property(path_id) + path_id_value = get_optional_property(path_id, include_private: true) required_attributes[path_id.to_s] = path_id_value unless path_id_value.nil? end @@ -456,7 +456,7 @@ def build_required_attributes(http_method:) sig { returns(Symbol) } def deduce_write_verb - get_property(self.class.primary_key) ? :put : :post + get_optional_property(self.class.primary_key, include_private: true) ? :put : :post end sig { params(method: Symbol).returns(T.nilable(String)) } @@ -504,6 +504,13 @@ def set_property(key, val) instance_variable_set("@#{clean_key}", val) end + sig { params(key: T.any(String, Symbol), include_private: T::Boolean).returns(T.untyped) } + def get_optional_property(key, include_private: false) + return nil unless respond_to?(key, include_private) + + include_private ? send(key) : public_send(key) + end + sig { params(key: T.any(String, Symbol)).returns(T.untyped) } def get_property(key) clean_key = @aliased_properties.key?(key.to_s) ? @aliased_properties[key.to_s] : key @@ -515,6 +522,8 @@ def get_property(key) def dynamic_property_defined?(key) property = @aliased_properties[key] || key !INTERNAL_PROPERTIES.include?(property) && instance_variable_defined?("@#{property}") + rescue NameError + false end sig do diff --git a/lib/shopify_api/rest/resources/2022_07/dispute_evidence.rb b/lib/shopify_api/rest/resources/2022_07/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2022_07/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2022_07/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2022_10/dispute_evidence.rb b/lib/shopify_api/rest/resources/2022_10/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2022_10/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2022_10/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2023_01/dispute_evidence.rb b/lib/shopify_api/rest/resources/2023_01/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2023_01/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2023_01/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2023_04/dispute_evidence.rb b/lib/shopify_api/rest/resources/2023_04/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2023_04/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2023_04/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2023_07/dispute_evidence.rb b/lib/shopify_api/rest/resources/2023_07/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2023_07/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2023_07/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2023_10/dispute_evidence.rb b/lib/shopify_api/rest/resources/2023_10/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2023_10/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2023_10/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2024_01/dispute_evidence.rb b/lib/shopify_api/rest/resources/2024_01/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2024_01/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2024_01/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2024_04/dispute_evidence.rb b/lib/shopify_api/rest/resources/2024_04/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2024_04/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2024_04/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2024_07/dispute_evidence.rb b/lib/shopify_api/rest/resources/2024_07/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2024_07/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2024_07/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2024_10/dispute_evidence.rb b/lib/shopify_api/rest/resources/2024_10/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2024_10/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2024_10/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2025_01/dispute_evidence.rb b/lib/shopify_api/rest/resources/2025_01/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2025_01/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2025_01/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2025_04/dispute_evidence.rb b/lib/shopify_api/rest/resources/2025_04/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2025_04/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2025_04/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2025_07/dispute_evidence.rb b/lib/shopify_api/rest/resources/2025_07/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2025_07/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2025_07/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2025_10/dispute_evidence.rb b/lib/shopify_api/rest/resources/2025_10/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2025_10/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2025_10/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2026_01/dispute_evidence.rb b/lib/shopify_api/rest/resources/2026_01/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2026_01/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2026_01/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2026_04/dispute_evidence.rb b/lib/shopify_api/rest/resources/2026_04/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2026_04/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2026_04/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2026_07/dispute_evidence.rb b/lib/shopify_api/rest/resources/2026_07/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2026_07/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2026_07/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/test/clients/base_rest_resource_test.rb b/test/clients/base_rest_resource_test.rb index 78a2e19c8..101c1b2ff 100644 --- a/test/clients/base_rest_resource_test.rb +++ b/test/clients/base_rest_resource_test.rb @@ -493,7 +493,7 @@ def test_put_requests_only_modify_changed_attributes product = ShopifyAPI::Product.find(id: 632910392, session: @session) - product.client.expects(:put).with( + product.instance_variable_get(:@client).expects(:put).with( body: { "product" => { "metafields" => [{ "key" => "new", "value" => "newvalue", "type" => "single_line_text_field", "namespace" => "global", }], "id" => 632910392, } }, path: "products/632910392.json", @@ -519,7 +519,7 @@ def test_put_request_for_has_one_association_works id: 207119551, session: @session, ) - customer.client.expects(:put).with( + customer.instance_variable_get(:@client).expects(:put).with( body: { "customer" => { "tags" => "New Customer, Repeat Customer", "id" => 207119551 } }, path: "customers/207119551.json", headers: nil, @@ -539,7 +539,7 @@ def test_put_request_with_atomic_hash_attributes id: 207119551, session: @session, ) - customer.client.expects(:put).with( + customer.instance_variable_get(:@client).expects(:put).with( body: { "customer" => { "id" => 207119551, "email_marketing_consent" => { "state" => "subscribed", "opt_in_level" => "single_opt_in", "consent_updated_at" => "2024-01-01T00:00:00.000Z" }, @@ -595,7 +595,7 @@ def test_put_requests_for_resource_with_read_only_attributes ) variant = ShopifyAPI::Variant.find(id: 169, session: @session) - variant.client.expects(:put).with( + variant.instance_variable_get(:@client).expects(:put).with( body: { "variant" => { "barcode" => "1234", "id" => 169 } }, path: "variants/169.json", headers: nil, diff --git a/test/rest/2022_04/customer_address_test.rb b/test/rest/2022_04/customer_address_test.rb index 18652bc89..cdf230c2d 100644 --- a/test/rest/2022_04/customer_address_test.rb +++ b/test/rest/2022_04/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2022_07/customer_address_test.rb b/test/rest/2022_07/customer_address_test.rb index 0b23b03fa..01820921c 100644 --- a/test/rest/2022_07/customer_address_test.rb +++ b/test/rest/2022_07/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2022_10/customer_address_test.rb b/test/rest/2022_10/customer_address_test.rb index 7f16f97d7..8f8fdc433 100644 --- a/test/rest/2022_10/customer_address_test.rb +++ b/test/rest/2022_10/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2023_01/customer_address_test.rb b/test/rest/2023_01/customer_address_test.rb index 68b1351c9..f0b0d4ed1 100644 --- a/test/rest/2023_01/customer_address_test.rb +++ b/test/rest/2023_01/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2023_04/customer_address_test.rb b/test/rest/2023_04/customer_address_test.rb index a074ba4ee..5d547de6b 100644 --- a/test/rest/2023_04/customer_address_test.rb +++ b/test/rest/2023_04/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2023_07/customer_address_test.rb b/test/rest/2023_07/customer_address_test.rb index d7e370726..b9b855da6 100644 --- a/test/rest/2023_07/customer_address_test.rb +++ b/test/rest/2023_07/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2023_10/customer_address_test.rb b/test/rest/2023_10/customer_address_test.rb index a6c63875e..07f7b45f6 100644 --- a/test/rest/2023_10/customer_address_test.rb +++ b/test/rest/2023_10/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2024_01/customer_address_test.rb b/test/rest/2024_01/customer_address_test.rb index 1107cc6cb..cbade086a 100644 --- a/test/rest/2024_01/customer_address_test.rb +++ b/test/rest/2024_01/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2024_04/customer_address_test.rb b/test/rest/2024_04/customer_address_test.rb index 454c44efa..ec4dce5ca 100644 --- a/test/rest/2024_04/customer_address_test.rb +++ b/test/rest/2024_04/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2024_07/customer_address_test.rb b/test/rest/2024_07/customer_address_test.rb index 10366aa9d..162223ff7 100644 --- a/test/rest/2024_07/customer_address_test.rb +++ b/test/rest/2024_07/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2024_10/customer_address_test.rb b/test/rest/2024_10/customer_address_test.rb index 73d3a6f0c..a0da6ff9e 100644 --- a/test/rest/2024_10/customer_address_test.rb +++ b/test/rest/2024_10/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2025_01/customer_address_test.rb b/test/rest/2025_01/customer_address_test.rb index 5e58f4e24..bb516e176 100644 --- a/test/rest/2025_01/customer_address_test.rb +++ b/test/rest/2025_01/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2025_04/customer_address_test.rb b/test/rest/2025_04/customer_address_test.rb index 4e71cf0c7..ea9c59b26 100644 --- a/test/rest/2025_04/customer_address_test.rb +++ b/test/rest/2025_04/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/base_test.rb b/test/rest/base_test.rb index 11ab3d2d5..f341195db 100644 --- a/test/rest/base_test.rb +++ b/test/rest/base_test.rb @@ -23,6 +23,25 @@ def test_missing_dynamic_getters_raise_and_are_not_reported_as_supported assert_equal(false, @product.respond_to?(:session)) assert_raises(NoMethodError) { @product.session } + + assert_equal(false, @product.respond_to?(:client)) + assert_raises(NoMethodError) { @product.client } + end + + def test_unknown_method_names_that_are_not_instance_variable_names + [:missing_predicate?, :missing_bang!, :"[]", :"unknown name"].each do |name| + assert_equal(false, @product.respond_to?(name)) + assert_equal(false, @product.respond_to?(name, true)) + error = assert_raises(NoMethodError) { @product.public_send(name) } + assert_equal(name, error.name) + end + end + + def test_unicode_dynamic_properties_remain_supported + @product.public_send(:café=, "value") + + assert_equal(true, @product.respond_to?(:café)) + assert_equal("value", @product.public_send(:café)) end def test_respond_to_recognises_dynamic_properties_including_nil_and_aliased_values @@ -57,4 +76,39 @@ def test_write_operations_treat_missing_resource_ids_as_nil ShopifyAPI::Product.get_path(http_method: :get, operation: :get, entity: feedback, ids: {}), ) end + + def test_optional_properties_preserve_computed_getters_and_visibility + payment = ShopifyAPI::Payment.new(session: @product.instance_variable_get(:@session)) + payment.checkout_id = "checkout-token" + + assert_equal("checkout-token", payment.send(:get_optional_property, :checkout_id)) + assert_equal( + "checkouts/checkout-token/payments.json", + ShopifyAPI::Payment.get_path(http_method: :post, operation: :post, entity: payment), + ) + assert_equal("checkout-token", payment.send(:build_required_attributes, http_method: :post)["checkout_id"]) + + @product.define_singleton_method(:id) { 123 } + assert_equal(:put, @product.send(:deduce_write_verb)) + assert_equal(123, @product.send(:build_required_attributes, http_method: :put)["id"]) + @product.define_singleton_method(:id) { nil } + assert_nil(@product.send(:get_optional_property, :id)) + assert_equal(:post, @product.send(:deduce_write_verb)) + assert_equal(true, @product.send(:get_optional_property, :"tax_exempt?")) + + @product.singleton_class.send(:private, :id) + assert_nil(@product.send(:get_optional_property, :id)) + @product.define_singleton_method(:id) { 456 } + @product.singleton_class.send(:private, :id) + assert_equal(456, @product.send(:get_optional_property, :id, include_private: true)) + assert_equal(:put, @product.send(:deduce_write_verb)) + assert_nil(ShopifyAPI::Product.get_path(http_method: :put, operation: :put, entity: @product)) + end + + def test_optional_properties_do_not_swallow_errors_inside_real_getters + @product.define_singleton_method(:id) { raise NoMethodError, "inside getter" } + + error = assert_raises(NoMethodError) { @product.send(:deduce_write_verb) } + assert_match(/inside getter/, error.message) + end end