diff --git a/lib/auth0/internal/types/model.rb b/lib/auth0/internal/types/model.rb index 28abff171..a08007830 100644 --- a/lib/auth0/internal/types/model.rb +++ b/lib/auth0/internal/types/model.rb @@ -141,13 +141,22 @@ def initialize(values = {}) values = Utils.symbolize_keys(values.dup) self.class.fields.each do |field_name, field| - value = values.delete(field.api_name.to_sym) || values.delete(field.api_name) || values.delete(field_name) + # key?/nil? checks keep an explicit `false`, and both keys are consumed so neither leaks into extra fields + value = nil + [field.api_name.to_sym, field_name].uniq.each do |key| + next unless values.key?(key) - field_value = value || (if field.literal? - field.value - elsif field.default - field.default - end) + candidate = values.delete(key) + value = candidate if value.nil? + end + + field_value = if !value.nil? + value + elsif field.literal? + field.value + elsif !field.default.nil? + field.default + end @data[field_name] = Utils.coerce(field.type, field_value) end diff --git a/test/unit/internal/types/test_model.rb b/test/unit/internal/types/test_model.rb index 785a98ac1..5cf0372b1 100644 --- a/test/unit/internal/types/test_model.rb +++ b/test/unit/internal/types/test_model.rb @@ -24,6 +24,12 @@ class ExampleWithDefaults < ExampleModel field :type, String, default: "example" end + class ExampleWithBooleans < Auth0::Internal::Types::Model + field :blocked, Auth0::Internal::Types::Boolean, optional: true + field :active, Auth0::Internal::Types::Boolean, optional: true, api_name: "isActive" + field :enabled, Auth0::Internal::Types::Boolean, optional: true, default: false + end + class ExampleChild < Auth0::Internal::Types::Model field :value, String end @@ -110,6 +116,42 @@ class ExampleParent < Auth0::Internal::Types::Model assert_equal 2014, example.year refute_respond_to example, :yearOfRelease end + + it "preserves false values on construction and in to_h" do + example = ExampleWithBooleans.new(blocked: false, active: false) + + assert_same false, example.blocked + assert_same false, example.active + assert_same false, example.to_h["blocked"] + assert_same false, example.to_h["isActive"] + end + + it "preserves false values when loaded from JSON" do + example = ExampleWithBooleans.load('{"blocked":false,"isActive":false}') + + assert_same false, example.blocked + assert_same false, example.active + end + + it "keeps an explicit false instead of falling back to the default" do + assert_same false, ExampleWithBooleans.new.enabled + assert_same true, ExampleWithBooleans.new(enabled: true).enabled + assert_same false, ExampleWithBooleans.new(enabled: false).enabled + end + + it "falls back to the field name when the api_name value is nil" do + example = ExampleWithBooleans.new(isActive: nil, active: true) + + assert_same true, example.active + refute_respond_to example, :isActive + end + + it "does not leak a consumed key into extra fields" do + example = ExampleWithBooleans.new(isActive: false, active: true) + + assert_same false, example.active + refute ExampleWithBooleans.extra_fields.key?(:active) + end end describe "#inspect" do