Commit aedfd65
Changed files (8)
lib
scim
spec
scim
kit
lib/scim/kit/cli/app.rb
@@ -4,12 +4,6 @@ module Scim
module Kit
module Cli
class App < Thor
- RESOURCES = {
- service_provider_configuration: 'ServiceProviderConfig',
- schemas: 'Schemas',
- resource_types: 'ResourceTypes'
- }.freeze
-
def self.exit_on_failure?
true
end
@@ -51,33 +45,16 @@ module Scim
end
def fetch_discovery
- responses = {}
- RESOURCES.each do |key, path|
- result = client.fetch(path)
- return reporter.report(result) unless result.ok?
-
- responses[key] = result.body
- end
- report_discovery(Http::Result.new(200, responses))
- end
-
- def report_discovery(combined)
- return reporter.report(combined) unless options[:validate]
-
- reporter.report_validation(combined, discovery_errors(combined.body))
- end
+ discovery = Discovery.new(client)
+ result = discovery.fetch
+ return reporter.report(result) unless result.ok? && options[:validate]
- def discovery_errors(responses)
- responses.each_with_object({}) do |(key, body), errors|
- schema = SchemaRegistry.fetch(key)
- body_errors = Validator.errors_for(schema, body)
- errors[key] = body_errors unless body_errors.empty?
- end
+ reporter.report_validation(result, discovery.errors_for(result.body))
end
def fetch_list(resource_type)
endpoint = resolve_endpoint(resource_type)
- result = client.fetch(endpoint, query: list_query)
+ result = client.fetch(endpoint, query: settings.list_query)
validate_and_report(result, resource_type) do |schema|
SchemaRegistry.list_response_with_items(schema)
end
@@ -114,86 +91,29 @@ module Scim
def validate_and_report(result, resource_type, &transform)
return reporter.report(result) unless result.ok? && options[:validate]
- schema = schema_for(resource_type)
- return reporter.report(result) unless schema
-
- errors = Validator.errors_for(
- prepare(schema, &transform), result.body
- )
- reporter.report_validation(result, errors)
- end
-
- def schema_for(resource_type)
entry = resource_type_entry(resource_type)
- schema = resource_schema_resolver.schema_for(entry)
- if schema
- warn_undeclared_extensions
- else
- warn_unresolvable_schema(resource_type)
- end
- schema
- end
+ errors = validation.errors_for(entry, result.body, &transform)
+ return reporter.report(result) unless errors
- def prepare(schema)
- schema = SparseSchema.relax(schema) if options[:attributes]
- block_given? ? yield(schema) : schema
+ reporter.report_validation(result, errors)
end
- def warn_unresolvable_schema(resource_type)
- reporter.warn(
- "no schema found for resource type #{resource_type.inspect}; " \
- 'skipping validation'
+ def validation
+ @validation ||= ResourceValidation.new(
+ resource_schema_resolver, reporter, sparse: !options[:attributes].nil?
)
end
- def warn_undeclared_extensions
- resource_schema_resolver.undeclared_extensions.each do |urn|
- reporter.warn(
- "schema extension #{urn.inspect} is declared by the resource " \
- 'type but missing from /Schemas'
- )
- end
- end
-
def reporter
@reporter ||= Reporter.new(shell)
end
- def list_query
- {
- 'filter' => options[:filter],
- 'startIndex' => options[:start_index],
- 'count' => options[:count],
- 'sortBy' => options[:sort_by],
- 'sortOrder' => options[:sort_order],
- 'attributes' => options[:attributes]
- }
- end
-
- def url
- @url ||= begin
- resolved = options[:url] || ENV.fetch('SCIM_KIT_URL', nil)
- raise Thor::Error, '--url is required' if resolved.to_s.empty?
-
- resolved
- end
- end
-
- def headers
- @headers ||= options[:header].each_with_object({}) do |header, hash|
- name, value = header.split(':', 2)
- if value.nil?
- raise Thor::Error,
- "malformed --header #{header.inspect} " \
- '(expected "Name: Value")'
- end
-
- hash[name.to_s.strip] = value.to_s.strip
- end
+ def settings
+ @settings ||= Settings.new(options)
end
def client
- @client ||= Client.new(url, headers: headers)
+ @client ||= Client.new(settings.url, headers: settings.headers)
end
end
end
lib/scim/kit/cli/discovery.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Scim
+ module Kit
+ module Cli
+ class Discovery
+ RESOURCES = {
+ service_provider_configuration: 'ServiceProviderConfig',
+ schemas: 'Schemas',
+ resource_types: 'ResourceTypes'
+ }.freeze
+
+ def initialize(client)
+ @client = client
+ end
+
+ def fetch
+ documents = {}
+ RESOURCES.each do |key, path|
+ result = client.fetch(path)
+ return result unless result.ok?
+
+ documents[key] = result.body
+ end
+ Http::Result.new(200, documents)
+ end
+
+ def errors_for(documents)
+ documents.each_with_object({}) do |(key, body), errors|
+ document_errors = Validator.errors_for(
+ SchemaRegistry.fetch(key), body
+ )
+ errors[key] = document_errors unless document_errors.empty?
+ end
+ end
+
+ private
+
+ attr_reader :client
+ end
+ end
+ end
+end
lib/scim/kit/cli/resource_validation.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Scim
+ module Kit
+ module Cli
+ class ResourceValidation
+ def initialize(resolver, reporter, sparse: false)
+ @resolver = resolver
+ @reporter = reporter
+ @sparse = sparse
+ end
+
+ def errors_for(entry, body, &transform)
+ schema = schema_for(entry)
+ return unless schema
+
+ Validator.errors_for(prepare(schema, &transform), body)
+ end
+
+ private
+
+ attr_reader :resolver, :reporter, :sparse
+
+ def schema_for(entry)
+ schema = resolver.schema_for(entry)
+ schema ? warn_undeclared_extensions : warn_unresolvable_schema(entry)
+ schema
+ end
+
+ def prepare(schema)
+ schema = SparseSchema.relax(schema) if sparse
+ block_given? ? yield(schema) : schema
+ end
+
+ def warn_unresolvable_schema(entry)
+ reporter.warn(
+ "no schema found for resource type #{entry[:name].inspect}; " \
+ 'skipping validation'
+ )
+ end
+
+ def warn_undeclared_extensions
+ resolver.undeclared_extensions.each do |urn|
+ reporter.warn(
+ "schema extension #{urn.inspect} is declared by the resource " \
+ 'type but missing from /Schemas'
+ )
+ end
+ end
+ end
+ end
+ end
+end
lib/scim/kit/cli/settings.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Scim
+ module Kit
+ module Cli
+ class Settings
+ QUERY = {
+ 'filter' => :filter,
+ 'startIndex' => :start_index,
+ 'count' => :count,
+ 'sortBy' => :sort_by,
+ 'sortOrder' => :sort_order,
+ 'attributes' => :attributes
+ }.freeze
+
+ def initialize(options, env: ENV)
+ @options = options
+ @env = env
+ end
+
+ def url
+ @url ||= begin
+ resolved = options[:url] || env.fetch('SCIM_KIT_URL', nil)
+ raise Thor::Error, '--url is required' if resolved.to_s.empty?
+
+ resolved
+ end
+ end
+
+ def headers
+ @headers ||= Array(options[:header]).to_h { |x| split_header(x) }
+ end
+
+ def list_query
+ QUERY.transform_values { |name| options[name] }
+ end
+
+ private
+
+ attr_reader :options, :env
+
+ def split_header(header)
+ name, value = header.split(':', 2)
+ if value.nil?
+ raise Thor::Error,
+ "malformed --header #{header.inspect} " \
+ '(expected "Name: Value")'
+ end
+
+ [name.to_s.strip, value.to_s.strip]
+ end
+ end
+ end
+ end
+end
lib/scim/kit/cli.rb
@@ -5,10 +5,13 @@ require 'thor'
require 'scim/kit/cli/app'
require 'scim/kit/cli/client'
+require 'scim/kit/cli/discovery'
require 'scim/kit/cli/reporter'
require 'scim/kit/cli/resource_schema_resolver'
require 'scim/kit/cli/resource_type_resolver'
+require 'scim/kit/cli/resource_validation'
require 'scim/kit/cli/schema_registry'
+require 'scim/kit/cli/settings'
require 'scim/kit/cli/scim_schema_converter'
require 'scim/kit/cli/sparse_schema'
require 'scim/kit/cli/validator'
spec/scim/kit/cli/discovery_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+RSpec.describe Scim::Kit::Cli::Discovery do
+ subject { described_class.new(Scim::Kit::Cli::Client.new(base_url)) }
+
+ let(:base_url) { FFaker::Internet.uri('https') }
+ let(:config) { { schemas: ['urn:x'], patch: { supported: true } } }
+ let(:schemas) { { totalResults: 0, Resources: [] } }
+ let(:resource_types) { { totalResults: 0, Resources: [] } }
+
+ before do
+ stub_request(:get, "#{base_url}/ServiceProviderConfig")
+ .to_return(status: 200, body: config.to_json)
+ stub_request(:get, "#{base_url}/Schemas")
+ .to_return(status: 200, body: schemas.to_json)
+ stub_request(:get, "#{base_url}/ResourceTypes")
+ .to_return(status: 200, body: resource_types.to_json)
+ end
+
+ describe '#fetch' do
+ it 'keys each document by its resource name' do
+ expect(subject.fetch.body.keys)
+ .to eql(%i[service_provider_configuration schemas resource_types])
+ end
+
+ it 'returns an ok result when every request succeeds' do
+ expect(subject.fetch).to be_ok
+ end
+
+ it 'collects each document body' do
+ expect(subject.fetch.body[:service_provider_configuration])
+ .to eql(config)
+ end
+
+ context 'when a request fails' do
+ before do
+ stub_request(:get, "#{base_url}/ServiceProviderConfig")
+ .to_return(status: 500, body: { detail: 'boom' }.to_json)
+ end
+
+ it 'returns the failed result' do
+ expect(subject.fetch).not_to be_ok
+ end
+
+ it 'stops before requesting the later documents' do
+ subject.fetch
+
+ expect(a_request(:get, "#{base_url}/Schemas")).not_to have_been_made
+ end
+ end
+ end
+
+ describe '#errors_for' do
+ it 'is empty when every document conforms' do
+ documents = subject.fetch.body
+ documents[:service_provider_configuration] = valid_config
+ documents[:schemas] = valid_list
+ documents[:resource_types] = valid_list
+
+ expect(subject.errors_for(documents)).to eql({})
+ end
+
+ it 'keys errors by the document that failed' do
+ errors = subject.errors_for(service_provider_configuration: {})
+
+ expect(errors.keys).to eql([:service_provider_configuration])
+ end
+
+ it 'omits documents that conform' do
+ documents = { schemas: valid_list, resource_types: {} }
+
+ expect(subject.errors_for(documents).keys).to eql([:resource_types])
+ end
+ end
+
+ def valid_config
+ {
+ schemas: ['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'],
+ patch: { supported: true },
+ bulk: { supported: false, maxOperations: 0, maxPayloadSize: 0 },
+ filter: { supported: false, maxResults: 0 },
+ changePassword: { supported: false }, sort: { supported: false },
+ etag: { supported: false }, authenticationSchemes: []
+ }
+ end
+
+ def valid_list
+ {
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
+ totalResults: 0, Resources: []
+ }
+ end
+end
spec/scim/kit/cli/settings_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+RSpec.describe Scim::Kit::Cli::Settings do
+ def settings(options, env: {})
+ described_class.new(options, env: env)
+ end
+
+ describe '#url' do
+ it 'prefers the --url option' do
+ expect(settings({ url: 'https://a' }).url).to eql('https://a')
+ end
+
+ it 'falls back to SCIM_KIT_URL' do
+ result = settings({}, env: { 'SCIM_KIT_URL' => 'https://b' })
+
+ expect(result.url).to eql('https://b')
+ end
+
+ it 'raises when neither is given' do
+ expect { settings({}).url }
+ .to raise_error(Thor::Error, /--url is required/)
+ end
+
+ it 'raises when the url is blank' do
+ expect { settings({ url: '' }).url }
+ .to raise_error(Thor::Error, /--url is required/)
+ end
+ end
+
+ describe '#headers' do
+ it 'parses a Name: Value pair' do
+ result = settings({ header: ['Authorization: Bearer xyz'] })
+
+ expect(result.headers).to eql('Authorization' => 'Bearer xyz')
+ end
+
+ it 'keeps colons in the value' do
+ result = settings({ header: ['X-A: a:b'] })
+
+ expect(result.headers).to eql('X-A' => 'a:b')
+ end
+
+ it 'parses repeated headers' do
+ result = settings({ header: ['A: 1', 'B: 2'] })
+
+ expect(result.headers).to eql('A' => '1', 'B' => '2')
+ end
+
+ it 'defaults to no headers' do
+ expect(settings({}).headers).to eql({})
+ end
+
+ it 'raises on a header without a colon' do
+ expect { settings({ header: ['nope'] }).headers }
+ .to raise_error(Thor::Error, /malformed --header/)
+ end
+ end
+
+ describe '#list_query' do
+ let(:options) do
+ { filter: 'a eq 1', start_index: 2, count: 3,
+ sort_by: 'userName', sort_order: 'ascending', attributes: 'id' }
+ end
+
+ it 'maps options onto their SCIM parameter names' do
+ expect(settings(options).list_query).to eql(
+ 'filter' => 'a eq 1', 'startIndex' => 2, 'count' => 3,
+ 'sortBy' => 'userName', 'sortOrder' => 'ascending', 'attributes' => 'id'
+ )
+ end
+
+ it 'leaves absent options nil for the client to drop' do
+ expect(settings({}).list_query.values).to all(be_nil)
+ end
+ end
+end
.rubocop.yml
@@ -58,10 +58,6 @@ Metrics/BlockLength:
- 'Rakefile'
- 'spec/**/*.rb'
-Metrics/ClassLength:
- Exclude:
- - 'lib/scim/kit/cli/app.rb'
-
Metrics/ModuleLength:
Exclude:
- 'spec/**/*.rb'