Commit 48d05c1
Changed files (5)
lib
scim
kit
spec
scim
lib/scim/kit/cli/unassigned_values.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Scim
+ module Kit
+ module Cli
+ # RFC 7643 2.5 makes the null value equivalent to an unassigned
+ # attribute, and omitting an unassigned attribute is only a MAY.
+ # Dropping nulls before validation is what makes the two spellings
+ # equivalent: "required" then reports an unassigned required attribute,
+ # and every other attribute is simply absent.
+ #
+ # 2.5 says the same of an empty array, but that is left alone on
+ # purpose: an empty collection is how a conformant server reports zero
+ # results, and "Resources" is only REQUIRED when "totalResults" is
+ # non-zero (RFC 7644 3.4.2).
+ module UnassignedValues
+ class << self
+ def strip(data)
+ case data
+ when Hash then assigned(data)
+ when Array then data.map { |value| strip(value) }
+ else data
+ end
+ end
+
+ private
+
+ # A null inside a multi-valued attribute is a value the server sent,
+ # not an unassigned attribute, so it stays and is reported.
+ def assigned(data)
+ data.each_with_object({}) do |(name, value), result|
+ result[name] = strip(value) unless value.nil?
+ end
+ end
+ end
+ end
+ end
+ end
+end
lib/scim/kit/cli/validator.rb
@@ -6,8 +6,9 @@ module Scim
module Validator
class << self
def errors_for(schema, data)
+ document = UnassignedValues.strip(normalize(data))
JSONSchemer.schema(schema)
- .validate(CanonicalKeys.apply(schema, normalize(data)))
+ .validate(CanonicalKeys.apply(schema, document))
.map { |error| JSONSchemer::Errors.pretty(error) }
end
lib/scim/kit/cli.rb
@@ -15,6 +15,7 @@ require 'scim/kit/cli/schema_registry'
require 'scim/kit/cli/scim_schema_converter'
require 'scim/kit/cli/settings'
require 'scim/kit/cli/sparse_schema'
+require 'scim/kit/cli/unassigned_values'
require 'scim/kit/cli/validator'
module Scim
spec/scim/kit/cli/unassigned_values_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.describe Scim::Kit::Cli::UnassignedValues do
+ describe '.strip' do
+ it { expect(described_class.strip('userName' => 'mo', 'externalId' => nil)).to eql('userName' => 'mo') }
+ it { expect(described_class.strip('Resources' => [])).to eql('Resources' => []) }
+ it { expect(described_class.strip('name' => { 'givenName' => nil })).to eql('name' => {}) }
+ it { expect(described_class.strip('emails' => [{ 'type' => nil }])).to eql('emails' => [{}]) }
+ it { expect(described_class.strip('emails' => [nil])).to eql('emails' => [nil]) }
+ it { expect(described_class.strip('active' => false)).to eql('active' => false) }
+ it { expect(described_class.strip('userName' => '')).to eql('userName' => '') }
+
+ it 'leaves the original document untouched' do
+ document = { 'externalId' => nil }
+
+ described_class.strip(document)
+
+ expect(document).to eql('externalId' => nil)
+ end
+ end
+end
spec/scim/kit/cli/validator_spec.rb
@@ -29,6 +29,64 @@ RSpec.describe Scim::Kit::Cli::Validator do
expect(errors).to eql(["property '/userName' is not of type: string"])
end
+ context 'with an optional attribute' do
+ let(:schema) do
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'userName' => { 'type' => 'string' },
+ 'externalId' => { 'type' => 'string' }
+ },
+ 'required' => ['userName']
+ }
+ end
+
+ it 'accepts null, which RFC 7643 2.5 makes equivalent to unassigned' do
+ errors = described_class.errors_for(
+ schema, { userName: 'bjensen', externalId: nil }
+ )
+
+ expect(errors).to eql([])
+ end
+
+ it 'still reports a wrong type' do
+ errors = described_class.errors_for(
+ schema, { userName: 'bjensen', externalId: 1 }
+ )
+
+ expect(errors).to eql(["property '/externalId' is not of type: string"])
+ end
+ end
+
+ it 'reports null for a required attribute as unassigned' do
+ errors = described_class.errors_for(schema, { userName: nil })
+
+ expect(errors).to eql(['root is missing required keys: userName'])
+ end
+
+ context 'with an optional attribute behind a $ref' do
+ let(:schema) do
+ {
+ 'type' => 'object',
+ 'properties' => { 'meta' => { '$ref' => '#/$defs/meta' } },
+ '$defs' => {
+ 'meta' => {
+ 'type' => 'object',
+ 'properties' => { 'version' => { 'type' => 'string' } }
+ }
+ }
+ }
+ end
+
+ it 'reports only the error the server actually made' do
+ errors = described_class.errors_for(schema, { meta: { version: 1 } })
+
+ expect(errors).to eql(
+ ["property '/meta/version' is not of type: string"]
+ )
+ end
+ end
+
it 'accepts a declared attribute the server spelled differently' do
errors = described_class.errors_for(schema, { USERNAME: 'bjensen' })