scimitar 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +671 -0
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +22 -4
- data/app/controllers/scimitar/application_controller.rb +7 -2
- data/app/controllers/scimitar/schemas_controller.rb +5 -0
- data/app/models/scimitar/errors.rb +1 -1
- data/app/models/scimitar/resources/base.rb +12 -2
- data/app/models/scimitar/schema/attribute.rb +15 -6
- data/app/models/scimitar/schema/base.rb +4 -2
- data/config/initializers/scimitar.rb +8 -3
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
- data/spec/apps/dummy/app/models/mock_user.rb +4 -0
- data/spec/apps/dummy/config/initializers/scimitar.rb +8 -0
- data/spec/apps/dummy/config/routes.rb +5 -0
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
- data/spec/apps/dummy/db/schema.rb +3 -2
- data/spec/controllers/scimitar/application_controller_spec.rb +2 -2
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +2 -2
- data/spec/controllers/scimitar/schemas_controller_spec.rb +8 -0
- data/spec/models/scimitar/lists/query_parser_spec.rb +1 -1
- data/spec/models/scimitar/resources/base_spec.rb +38 -4
- data/spec/models/scimitar/resources/mixin_spec.rb +4 -1
- data/spec/models/scimitar/resources/user_spec.rb +4 -4
- data/spec/models/scimitar/schema/attribute_spec.rb +22 -0
- data/spec/requests/active_record_backed_resources_controller_spec.rb +127 -37
- data/spec/requests/application_controller_spec.rb +1 -2
- metadata +7 -3
| @@ -153,7 +153,13 @@ module Scimitar | |
| 153 153 | 
             
                  # Save a record, dealing with validation exceptions by raising SCIM
         | 
| 154 154 | 
             
                  # errors.
         | 
| 155 155 | 
             
                  #
         | 
| 156 | 
            -
                  # +record+:: ActiveRecord subclass to save | 
| 156 | 
            +
                  # +record+:: ActiveRecord subclass to save.
         | 
| 157 | 
            +
                  #
         | 
| 158 | 
            +
                  # If you just let this superclass handle things, it'll call the standard
         | 
| 159 | 
            +
                  # +#save!+ method on the record. If you pass a block, then this block is
         | 
| 160 | 
            +
                  # invoked and passed the ActiveRecord model instance to be saved. You can
         | 
| 161 | 
            +
                  # then do things like calling a different method, using a service object
         | 
| 162 | 
            +
                  # of some kind, perform audit-related operations and so-on.
         | 
| 157 163 | 
             
                  #
         | 
| 158 164 | 
             
                  # The return value is not used internally, making life easier for
         | 
| 159 165 | 
             
                  # overriding subclasses to "do the right thing" / avoid mistakes (instead
         | 
| @@ -161,10 +167,22 @@ module Scimitar | |
| 161 167 | 
             
                  # and relying upon this to generate correct response payloads - an early
         | 
| 162 168 | 
             
                  # version of the gem did this and it caused a confusing subclass bug).
         | 
| 163 169 | 
             
                  #
         | 
| 164 | 
            -
                  def save!(record)
         | 
| 165 | 
            -
                     | 
| 166 | 
            -
             | 
| 170 | 
            +
                  def save!(record, &block)
         | 
| 171 | 
            +
                    if block_given?
         | 
| 172 | 
            +
                      yield(record)
         | 
| 173 | 
            +
                    else
         | 
| 174 | 
            +
                      record.save!
         | 
| 175 | 
            +
                    end
         | 
| 167 176 | 
             
                  rescue ActiveRecord::RecordInvalid => exception
         | 
| 177 | 
            +
                    handle_invalid_record(exception.record)
         | 
| 178 | 
            +
                  end
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                  # Deal with validation errors by responding with an appropriate SCIM
         | 
| 181 | 
            +
                  # error.
         | 
| 182 | 
            +
                  #
         | 
| 183 | 
            +
                  # +record+:: The record with validation errors.
         | 
| 184 | 
            +
                  #
         | 
| 185 | 
            +
                  def handle_invalid_record(record)
         | 
| 168 186 | 
             
                    joined_errors = record.errors.full_messages.join('; ')
         | 
| 169 187 |  | 
| 170 188 | 
             
                    # https://tools.ietf.org/html/rfc7644#page-12
         | 
| @@ -124,8 +124,13 @@ module Scimitar | |
| 124 124 | 
             
                    #
         | 
| 125 125 | 
             
                    # https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
         | 
| 126 126 | 
             
                    #
         | 
| 127 | 
            -
                    response.set_header(' | 
| 128 | 
            -
                    response.set_header(' | 
| 127 | 
            +
                    response.set_header('WWW-Authenticate', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
         | 
| 128 | 
            +
                    response.set_header('WWW-Authenticate', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    # No matter what a caller might request via headers, the only content
         | 
| 131 | 
            +
                    # type we can ever respond with is JSON-for-SCIM.
         | 
| 132 | 
            +
                    #
         | 
| 133 | 
            +
                    response.set_header('Content-Type', "#{Mime::Type.lookup_by_extension(:scim)}; charset=utf-8")
         | 
| 129 134 | 
             
                  end
         | 
| 130 135 |  | 
| 131 136 | 
             
                  def authenticate
         | 
| @@ -4,6 +4,11 @@ module Scimitar | |
| 4 4 | 
             
              class SchemasController < ApplicationController
         | 
| 5 5 | 
             
                def index
         | 
| 6 6 | 
             
                  schemas = Scimitar::Engine.schemas
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  schemas.each do |schema|
         | 
| 9 | 
            +
                    schema.meta.location = scim_schemas_url(name: schema.id)
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 7 12 | 
             
                  schemas_by_id = schemas.reduce({}) do |hash, schema|
         | 
| 8 13 | 
             
                    hash[schema.id] = schema
         | 
| 9 14 | 
             
                    hash
         | 
| @@ -139,13 +139,23 @@ module Scimitar | |
| 139 139 |  | 
| 140 140 | 
             
                  def as_json(options = {})
         | 
| 141 141 | 
             
                    self.meta = Meta.new unless self.meta && self.meta.is_a?(Meta)
         | 
| 142 | 
            -
                    meta.resourceType = self.class.resource_type_id
         | 
| 143 | 
            -
             | 
| 142 | 
            +
                    self.meta.resourceType = self.class.resource_type_id
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                    non_returnable_attributes = self.class
         | 
| 145 | 
            +
                      .schemas
         | 
| 146 | 
            +
                      .flat_map(&:scim_attributes)
         | 
| 147 | 
            +
                      .filter_map { |attribute| attribute.name if attribute.returned == 'never' }
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                    non_returnable_attributes << 'errors'
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                    original_hash = super(options).except(*non_returnable_attributes)
         | 
| 144 152 | 
             
                    original_hash.merge!('schemas' => self.class.schemas.map(&:id))
         | 
| 153 | 
            +
             | 
| 145 154 | 
             
                    self.class.extended_schemas.each do |extension_schema|
         | 
| 146 155 | 
             
                      extension_attributes = extension_schema.scim_attributes.map(&:name)
         | 
| 147 156 | 
             
                      original_hash.merge!(extension_schema.id => original_hash.extract!(*extension_attributes))
         | 
| 148 157 | 
             
                    end
         | 
| 158 | 
            +
             | 
| 149 159 | 
             
                    original_hash
         | 
| 150 160 | 
             
                  end
         | 
| 151 161 |  | 
| @@ -88,19 +88,28 @@ module Scimitar | |
| 88 88 | 
             
                    end
         | 
| 89 89 | 
             
                    value.class.schema.valid?(value)
         | 
| 90 90 | 
             
                    return true if value.errors.empty?
         | 
| 91 | 
            -
                    add_errors_from_hash(value.errors.to_hash, prefix: self.name)
         | 
| 91 | 
            +
                    add_errors_from_hash(errors_hash: value.errors.to_hash, prefix: self.name)
         | 
| 92 92 | 
             
                    false
         | 
| 93 93 | 
             
                  end
         | 
| 94 94 |  | 
| 95 95 | 
             
                  def valid_simple_type?(value)
         | 
| 96 | 
            -
                     | 
| 97 | 
            -
                       | 
| 98 | 
            -
                      (type  | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 96 | 
            +
                    if multiValued
         | 
| 97 | 
            +
                      valid = value.is_a?(Array) && value.all? { |v| simple_type?(v) }
         | 
| 98 | 
            +
                      errors.add(self.name, "or one of its elements has the wrong type. It has to be an array of #{self.type}s.") unless valid
         | 
| 99 | 
            +
                    else
         | 
| 100 | 
            +
                      valid = simple_type?(value)
         | 
| 101 | 
            +
                      errors.add(self.name, "has the wrong type. It has to be a(n) #{self.type}.") unless valid
         | 
| 102 | 
            +
                    end
         | 
| 101 103 | 
             
                    valid
         | 
| 102 104 | 
             
                  end
         | 
| 103 105 |  | 
| 106 | 
            +
                  def simple_type?(value)
         | 
| 107 | 
            +
                    (type == 'string' && value.is_a?(String)) ||
         | 
| 108 | 
            +
                    (type == 'boolean' && (value.is_a?(TrueClass) || value.is_a?(FalseClass))) ||
         | 
| 109 | 
            +
                    (type == 'integer' && (value.is_a?(Integer))) ||
         | 
| 110 | 
            +
                    (type == 'dateTime' && valid_date_time?(value))
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
             | 
| 104 113 | 
             
                  def valid_date_time?(value)
         | 
| 105 114 | 
             
                    !!Time.iso8601(value)
         | 
| 106 115 | 
             
                  rescue ArgumentError
         | 
| @@ -13,7 +13,7 @@ module Scimitar | |
| 13 13 |  | 
| 14 14 | 
             
                  # Converts the schema to its json representation that will be returned by /SCHEMAS end-point of a SCIM service provider.
         | 
| 15 15 | 
             
                  def as_json(options = {})
         | 
| 16 | 
            -
                    @meta.location  | 
| 16 | 
            +
                    @meta.location ||= Scimitar::Engine.routes.url_helpers.scim_schemas_path(name: id)
         | 
| 17 17 | 
             
                    original = super
         | 
| 18 18 | 
             
                    original.merge('attributes' => original.delete('scim_attributes'))
         | 
| 19 19 | 
             
                  end
         | 
| @@ -26,7 +26,9 @@ module Scimitar | |
| 26 26 | 
             
                  #
         | 
| 27 27 | 
             
                  def self.valid?(resource)
         | 
| 28 28 | 
             
                    cloned_scim_attributes.each do |scim_attribute|
         | 
| 29 | 
            -
                       | 
| 29 | 
            +
                      unless scim_attribute.valid?(resource.send(scim_attribute.name))
         | 
| 30 | 
            +
                        resource.add_errors_from_hash(errors_hash: scim_attribute.errors.to_hash)
         | 
| 31 | 
            +
                      end
         | 
| 30 32 | 
             
                    end
         | 
| 31 33 | 
             
                  end
         | 
| 32 34 |  | 
| @@ -38,9 +38,10 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk) | |
| 38 38 | 
             
              Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
         | 
| 39 39 |  | 
| 40 40 | 
             
                # If you have filters you want to run for any Scimitar action/route, you
         | 
| 41 | 
            -
                # can define them here.  | 
| 42 | 
            -
                #  | 
| 43 | 
            -
                # verification | 
| 41 | 
            +
                # can define them here. You can also override any shared controller methods
         | 
| 42 | 
            +
                # here. For example, you might use a before-action to set up some
         | 
| 43 | 
            +
                # multi-tenancy related state, skip Rails CSRF token verification, or
         | 
| 44 | 
            +
                # customise how Scimitar generates URLs:
         | 
| 44 45 | 
             
                #
         | 
| 45 46 | 
             
                #     application_controller_mixin: Module.new do
         | 
| 46 47 | 
             
                #       def self.included(base)
         | 
| @@ -54,6 +55,10 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk) | |
| 54 55 | 
             
                #           prepend_before_action :setup_some_kind_of_multi_tenancy_data
         | 
| 55 56 | 
             
                #         end
         | 
| 56 57 | 
             
                #       end
         | 
| 58 | 
            +
                #
         | 
| 59 | 
            +
                #       def scim_schemas_url(options)
         | 
| 60 | 
            +
                #         super(custom_param: 'value', **options)
         | 
| 61 | 
            +
                #       end
         | 
| 57 62 | 
             
                #     end, # ...other configuration entries might follow...
         | 
| 58 63 |  | 
| 59 64 | 
             
                # If you want to support username/password authentication:
         | 
    
        data/lib/scimitar/version.rb
    CHANGED
    
    | @@ -3,11 +3,11 @@ module Scimitar | |
| 3 3 | 
             
              # Gem version. If this changes, be sure to re-run "bundle install" or
         | 
| 4 4 | 
             
              # "bundle update".
         | 
| 5 5 | 
             
              #
         | 
| 6 | 
            -
              VERSION = '1. | 
| 6 | 
            +
              VERSION = '1.7.1'
         | 
| 7 7 |  | 
| 8 8 | 
             
              # Date for VERSION. If this changes, be sure to re-run "bundle install"
         | 
| 9 9 | 
             
              # or "bundle update".
         | 
| 10 10 | 
             
              #
         | 
| 11 | 
            -
              DATE = '2023- | 
| 11 | 
            +
              DATE = '2023-11-15'
         | 
| 12 12 |  | 
| 13 13 | 
             
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            # For tests only - uses custom 'save!' implementation which passes a block to
         | 
| 2 | 
            +
            # Scimitar::ActiveRecordBackedResourcesController#save!.
         | 
| 3 | 
            +
            #
         | 
| 4 | 
            +
            class CustomSaveMockUsersController < Scimitar::ActiveRecordBackedResourcesController
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              CUSTOM_SAVE_BLOCK_USERNAME_INDICATOR = 'Custom save-block invoked'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              protected
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def save!(_record)
         | 
| 11 | 
            +
                  super do | record |
         | 
| 12 | 
            +
                    record.update!(username: CUSTOM_SAVE_BLOCK_USERNAME_INDICATOR)
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def storage_class
         | 
| 17 | 
            +
                  MockUser
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def storage_scope
         | 
| 21 | 
            +
                  MockUser.all
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            end
         | 
| @@ -1,5 +1,7 @@ | |
| 1 1 | 
             
            class MockUser < ActiveRecord::Base
         | 
| 2 2 |  | 
| 3 | 
            +
              self.primary_key = :primary_key
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
              # ===========================================================================
         | 
| 4 6 | 
             
              # TEST ATTRIBUTES - see db/migrate/20210304014602_create_mock_users.rb etc.
         | 
| 5 7 | 
             
              # ===========================================================================
         | 
| @@ -8,6 +10,7 @@ class MockUser < ActiveRecord::Base | |
| 8 10 | 
             
                primary_key
         | 
| 9 11 | 
             
                scim_uid
         | 
| 10 12 | 
             
                username
         | 
| 13 | 
            +
                password
         | 
| 11 14 | 
             
                first_name
         | 
| 12 15 | 
             
                last_name
         | 
| 13 16 | 
             
                work_email_address
         | 
| @@ -44,6 +47,7 @@ class MockUser < ActiveRecord::Base | |
| 44 47 | 
             
                  id:         :primary_key,
         | 
| 45 48 | 
             
                  externalId: :scim_uid,
         | 
| 46 49 | 
             
                  userName:   :username,
         | 
| 50 | 
            +
                  password:   :password,
         | 
| 47 51 | 
             
                  name:       {
         | 
| 48 52 | 
             
                    givenName:  :first_name,
         | 
| 49 53 | 
             
                    familyName: :last_name
         | 
| @@ -26,6 +26,14 @@ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({ | |
| 26 26 | 
             
                    before_action :test_hook
         | 
| 27 27 | 
             
                  end
         | 
| 28 28 | 
             
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def scim_schemas_url(options)
         | 
| 31 | 
            +
                  super(test: 1, **options)
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def scim_resource_type_url(options)
         | 
| 35 | 
            +
                  super(test: 1, **options)
         | 
| 36 | 
            +
                end
         | 
| 29 37 | 
             
              end
         | 
| 30 38 |  | 
| 31 39 | 
             
            })
         | 
| @@ -21,6 +21,11 @@ Rails.application.routes.draw do | |
| 21 21 | 
             
              #
         | 
| 22 22 | 
             
              delete 'CustomDestroyUsers/:id', to: 'custom_destroy_mock_users#destroy'
         | 
| 23 23 |  | 
| 24 | 
            +
              # For testing blocks passed to ActiveRecordBackedResourcesController#save!
         | 
| 25 | 
            +
              #
         | 
| 26 | 
            +
              post 'CustomSaveUsers',     to: 'custom_save_mock_users#create'
         | 
| 27 | 
            +
              get  'CustomSaveUsers/:id', to: 'custom_save_mock_users#show'
         | 
| 28 | 
            +
             | 
| 24 29 | 
             
              # For testing environment inside Scimitar::ApplicationController subclasses.
         | 
| 25 30 | 
             
              #
         | 
| 26 31 | 
             
              get  'CustomRequestVerifiers', to: 'custom_request_verifiers#index'
         | 
| @@ -30,10 +30,11 @@ ActiveRecord::Schema.define(version: 2021_03_08_044214) do | |
| 30 30 | 
             
              end
         | 
| 31 31 |  | 
| 32 32 | 
             
              create_table "mock_users", primary_key: "primary_key", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
         | 
| 33 | 
            -
                t.datetime "created_at",  | 
| 34 | 
            -
                t.datetime "updated_at",  | 
| 33 | 
            +
                t.datetime "created_at", null: false
         | 
| 34 | 
            +
                t.datetime "updated_at", null: false
         | 
| 35 35 | 
             
                t.text "scim_uid"
         | 
| 36 36 | 
             
                t.text "username"
         | 
| 37 | 
            +
                t.text "password"
         | 
| 37 38 | 
             
                t.text "first_name"
         | 
| 38 39 | 
             
                t.text "last_name"
         | 
| 39 40 | 
             
                t.text "work_email_address"
         | 
| @@ -24,7 +24,7 @@ RSpec.describe Scimitar::ApplicationController do | |
| 24 24 | 
             
                  get :index, params: { format: :scim }
         | 
| 25 25 | 
             
                  expect(response).to be_ok
         | 
| 26 26 | 
             
                  expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
         | 
| 27 | 
            -
                  expect(response.headers[' | 
| 27 | 
            +
                  expect(response.headers['WWW-Authenticate']).to eql('Basic')
         | 
| 28 28 | 
             
                end
         | 
| 29 29 |  | 
| 30 30 | 
             
                it 'renders failure with bad password' do
         | 
| @@ -84,7 +84,7 @@ RSpec.describe Scimitar::ApplicationController do | |
| 84 84 | 
             
                  get :index, params: { format: :scim }
         | 
| 85 85 | 
             
                  expect(response).to be_ok
         | 
| 86 86 | 
             
                  expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
         | 
| 87 | 
            -
                  expect(response.headers[' | 
| 87 | 
            +
                  expect(response.headers['WWW-Authenticate']).to eql('Bearer')
         | 
| 88 88 | 
             
                end
         | 
| 89 89 |  | 
| 90 90 | 
             
                it 'renders failure with bad token' do
         | 
| @@ -9,8 +9,8 @@ RSpec.describe Scimitar::ResourceTypesController do | |
| 9 9 | 
             
                it 'renders the resource type for user' do
         | 
| 10 10 | 
             
                  get :index, format: :scim
         | 
| 11 11 | 
             
                  response_hash = JSON.parse(response.body)
         | 
| 12 | 
            -
                  expected_response = [ Scimitar::Resources::User.resource_type(scim_resource_type_url(name: 'User')),
         | 
| 13 | 
            -
                                        Scimitar::Resources::Group.resource_type(scim_resource_type_url(name: 'Group'))
         | 
| 12 | 
            +
                  expected_response = [ Scimitar::Resources::User.resource_type(scim_resource_type_url(name: 'User', test: 1)),
         | 
| 13 | 
            +
                                        Scimitar::Resources::Group.resource_type(scim_resource_type_url(name: 'Group', test: 1))
         | 
| 14 14 | 
             
                  ].to_json
         | 
| 15 15 |  | 
| 16 16 | 
             
                  response_hash = JSON.parse(response.body)
         | 
| @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            require 'spec_helper'
         | 
| 2 2 |  | 
| 3 3 | 
             
            RSpec.describe Scimitar::SchemasController do
         | 
| 4 | 
            +
              routes { Scimitar::Engine.routes }
         | 
| 4 5 |  | 
| 5 6 | 
             
              before(:each) { allow(controller).to receive(:authenticated?).and_return(true) }
         | 
| 6 7 |  | 
| @@ -26,6 +27,13 @@ RSpec.describe Scimitar::SchemasController do | |
| 26 27 | 
             
                  expect(parsed_body['name']).to eql('User')
         | 
| 27 28 | 
             
                end
         | 
| 28 29 |  | 
| 30 | 
            +
                it 'includes the controller customised schema location' do
         | 
| 31 | 
            +
                  get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
         | 
| 32 | 
            +
                  expect(response).to be_ok
         | 
| 33 | 
            +
                  parsed_body = JSON.parse(response.body)
         | 
| 34 | 
            +
                  expect(parsed_body.dig('meta', 'location')).to eq scim_schemas_url(name: Scimitar::Schema::User.id, test: 1)
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 29 37 | 
             
                it 'returns only the Group schema when its id is provided' do
         | 
| 30 38 | 
             
                  get :index, params: { name: Scimitar::Schema::Group.id, format: :scim }
         | 
| 31 39 | 
             
                  expect(response).to be_ok
         | 
| @@ -405,7 +405,7 @@ RSpec.describe Scimitar::Lists::QueryParser do | |
| 405 405 | 
             
                  query = @instance.to_activerecord_query(MockUser.all)
         | 
| 406 406 |  | 
| 407 407 | 
             
                  expect(query.count).to eql(1)
         | 
| 408 | 
            -
                  expect(query.pluck(:primary_key)).to eql([user_1. | 
| 408 | 
            +
                  expect(query.pluck(:primary_key)).to eql([user_1.primary_key])
         | 
| 409 409 |  | 
| 410 410 | 
             
                  @instance.parse('name.givenName sw J') # First name starts with 'J'
         | 
| 411 411 | 
             
                  query = @instance.to_activerecord_query(MockUser.all)
         | 
| @@ -14,7 +14,10 @@ RSpec.describe Scimitar::Resources::Base do | |
| 14 14 | 
             
                      ),
         | 
| 15 15 | 
             
                      Scimitar::Schema::Attribute.new(
         | 
| 16 16 | 
             
                        name: 'names', multiValued: true, complexType: Scimitar::ComplexTypes::Name, required: false
         | 
| 17 | 
            -
                      )
         | 
| 17 | 
            +
                      ),
         | 
| 18 | 
            +
                      Scimitar::Schema::Attribute.new(
         | 
| 19 | 
            +
                        name: 'privateName', complexType: Scimitar::ComplexTypes::Name, required: false, returned: false
         | 
| 20 | 
            +
                      ),
         | 
| 18 21 | 
             
                    ]
         | 
| 19 22 | 
             
                  end
         | 
| 20 23 | 
             
                end
         | 
| @@ -30,6 +33,10 @@ RSpec.describe Scimitar::Resources::Base do | |
| 30 33 | 
             
                        name: {
         | 
| 31 34 | 
             
                          givenName:  'John',
         | 
| 32 35 | 
             
                          familyName: 'Smith'
         | 
| 36 | 
            +
                        },
         | 
| 37 | 
            +
                        privateName: {
         | 
| 38 | 
            +
                          givenName:  'Alt John',
         | 
| 39 | 
            +
                          familyName: 'Alt Smith'
         | 
| 33 40 | 
             
                        }
         | 
| 34 41 | 
             
                      }
         | 
| 35 42 |  | 
| @@ -39,6 +46,9 @@ RSpec.describe Scimitar::Resources::Base do | |
| 39 46 | 
             
                      expect(resource.name.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
         | 
| 40 47 | 
             
                      expect(resource.name.givenName).to eql('John')
         | 
| 41 48 | 
             
                      expect(resource.name.familyName).to eql('Smith')
         | 
| 49 | 
            +
                      expect(resource.privateName.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
         | 
| 50 | 
            +
                      expect(resource.privateName.givenName).to eql('Alt John')
         | 
| 51 | 
            +
                      expect(resource.privateName.familyName).to eql('Alt Smith')
         | 
| 42 52 | 
             
                    end
         | 
| 43 53 |  | 
| 44 54 | 
             
                    it 'which builds an array of nested resources' do
         | 
| @@ -101,14 +111,38 @@ RSpec.describe Scimitar::Resources::Base do | |
| 101 111 | 
             
                context '#as_json' do
         | 
| 102 112 | 
             
                  it 'renders the json with the resourceType' do
         | 
| 103 113 | 
             
                    resource = CustomResourse.new(name: {
         | 
| 104 | 
            -
                      givenName: | 
| 114 | 
            +
                      givenName:  'John',
         | 
| 105 115 | 
             
                      familyName: 'Smith'
         | 
| 106 116 | 
             
                    })
         | 
| 107 117 |  | 
| 108 118 | 
             
                    result = resource.as_json
         | 
| 109 | 
            -
             | 
| 119 | 
            +
             | 
| 120 | 
            +
                    expect(result['schemas']             ).to eql(['custom-id'])
         | 
| 121 | 
            +
                    expect(result['meta']['resourceType']).to eql('CustomResourse')
         | 
| 122 | 
            +
                    expect(result['errors']              ).to be_nil
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                  it 'excludes attributes that are flagged as do-not-return' do
         | 
| 126 | 
            +
                    resource = CustomResourse.new(
         | 
| 127 | 
            +
                      name: {
         | 
| 128 | 
            +
                        givenName:  'John',
         | 
| 129 | 
            +
                        familyName: 'Smith'
         | 
| 130 | 
            +
                      },
         | 
| 131 | 
            +
                      privateName: {
         | 
| 132 | 
            +
                        givenName:  'Alt John',
         | 
| 133 | 
            +
                        familyName: 'Alt Smith'
         | 
| 134 | 
            +
                      }
         | 
| 135 | 
            +
                    )
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                    result = resource.as_json
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                    expect(result['schemas']             ).to eql(['custom-id'])
         | 
| 110 140 | 
             
                    expect(result['meta']['resourceType']).to eql('CustomResourse')
         | 
| 111 | 
            -
                    expect(result['errors']).to be_nil
         | 
| 141 | 
            +
                    expect(result['errors']              ).to be_nil
         | 
| 142 | 
            +
                    expect(result['name']                ).to be_present
         | 
| 143 | 
            +
                    expect(result['name']['givenName']   ).to eql('John')
         | 
| 144 | 
            +
                    expect(result['name']['familyName']  ).to eql('Smith')
         | 
| 145 | 
            +
                    expect(result['privateName']         ).to be_present
         | 
| 112 146 | 
             
                  end
         | 
| 113 147 | 
             
                end # "context '#as_json' do"
         | 
| 114 148 |  | 
| @@ -160,13 +160,14 @@ RSpec.describe Scimitar::Resources::Mixin do | |
| 160 160 |  | 
| 161 161 | 
             
                context '#to_scim' do
         | 
| 162 162 | 
             
                  context 'with a UUID, renamed primary key column' do
         | 
| 163 | 
            -
                    it 'compiles instance attribute values into a SCIM representation' do
         | 
| 163 | 
            +
                    it 'compiles instance attribute values into a SCIM representation, but omits do-not-return fields' do
         | 
| 164 164 | 
             
                      uuid                        = SecureRandom.uuid
         | 
| 165 165 |  | 
| 166 166 | 
             
                      instance                    = MockUser.new
         | 
| 167 167 | 
             
                      instance.primary_key        = uuid
         | 
| 168 168 | 
             
                      instance.scim_uid           = 'AA02984'
         | 
| 169 169 | 
             
                      instance.username           = 'foo'
         | 
| 170 | 
            +
                      instance.password           = 'correcthorsebatterystaple'
         | 
| 170 171 | 
             
                      instance.first_name         = 'Foo'
         | 
| 171 172 | 
             
                      instance.last_name          = 'Bar'
         | 
| 172 173 | 
             
                      instance.work_email_address = 'foo.bar@test.com'
         | 
| @@ -404,6 +405,7 @@ RSpec.describe Scimitar::Resources::Mixin do | |
| 404 405 | 
             
                      it 'ignoring read-only lists' do
         | 
| 405 406 | 
             
                        hash = {
         | 
| 406 407 | 
             
                          'userName'     => 'foo',
         | 
| 408 | 
            +
                          'password'     => 'staplebatteryhorsecorrect',
         | 
| 407 409 | 
             
                          'name'         => {'givenName' => 'Foo', 'familyName' => 'Bar'},
         | 
| 408 410 | 
             
                          'active'       => true,
         | 
| 409 411 | 
             
                          'emails'       => [{'type' => 'work',  'primary' => true,  'value' => 'foo.bar@test.com'}],
         | 
| @@ -428,6 +430,7 @@ RSpec.describe Scimitar::Resources::Mixin do | |
| 428 430 |  | 
| 429 431 | 
             
                        expect(instance.scim_uid          ).to eql('AA02984')
         | 
| 430 432 | 
             
                        expect(instance.username          ).to eql('foo')
         | 
| 433 | 
            +
                        expect(instance.password          ).to eql('staplebatteryhorsecorrect')
         | 
| 431 434 | 
             
                        expect(instance.first_name        ).to eql('Foo')
         | 
| 432 435 | 
             
                        expect(instance.last_name         ).to eql('Bar')
         | 
| 433 436 | 
             
                        expect(instance.work_email_address).to eql('foo.bar@test.com')
         | 
| @@ -42,25 +42,25 @@ RSpec.describe Scimitar::Resources::User do | |
| 42 42 | 
             
                let(:user) { described_class.new }
         | 
| 43 43 |  | 
| 44 44 | 
             
                it 'adds the error when the value is a string' do
         | 
| 45 | 
            -
                  user.add_errors_from_hash({key: 'some error'})
         | 
| 45 | 
            +
                  user.add_errors_from_hash(errors_hash: {key: 'some error'})
         | 
| 46 46 | 
             
                  expect(user.errors.messages.to_h).to eql({key: ['some error']})
         | 
| 47 47 | 
             
                  expect(user.errors.full_messages).to eql(['Key some error'])
         | 
| 48 48 | 
             
                end
         | 
| 49 49 |  | 
| 50 50 | 
             
                it 'adds the error when the value is an array' do
         | 
| 51 | 
            -
                  user.add_errors_from_hash({key: ['error1', 'error2']})
         | 
| 51 | 
            +
                  user.add_errors_from_hash(errors_hash: {key: ['error1', 'error2']})
         | 
| 52 52 | 
             
                  expect(user.errors.messages.to_h).to eql({key: ['error1', 'error2']})
         | 
| 53 53 | 
             
                  expect(user.errors.full_messages).to eql(['Key error1', 'Key error2'])
         | 
| 54 54 | 
             
                end
         | 
| 55 55 |  | 
| 56 56 | 
             
                it 'adds the error with prefix when the value is a string' do
         | 
| 57 | 
            -
                  user.add_errors_from_hash({key: 'some error'}, prefix: :pre)
         | 
| 57 | 
            +
                  user.add_errors_from_hash(errors_hash: {key: 'some error'}, prefix: :pre)
         | 
| 58 58 | 
             
                  expect(user.errors.messages.to_h).to eql({:'pre.key' => ['some error']})
         | 
| 59 59 | 
             
                  expect(user.errors.full_messages).to eql(['Pre key some error'])
         | 
| 60 60 | 
             
                end
         | 
| 61 61 |  | 
| 62 62 | 
             
                it 'adds the error wity prefix when the value is an array' do
         | 
| 63 | 
            -
                  user.add_errors_from_hash({key: ['error1', 'error2']}, prefix: :pre)
         | 
| 63 | 
            +
                  user.add_errors_from_hash(errors_hash: {key: ['error1', 'error2']}, prefix: :pre)
         | 
| 64 64 | 
             
                  expect(user.errors.messages.to_h).to eql({:'pre.key' => ['error1', 'error2']})
         | 
| 65 65 | 
             
                  expect(user.errors.full_messages).to eql(['Pre key error1', 'Pre key error2'])
         | 
| 66 66 | 
             
                end
         | 
| @@ -46,6 +46,28 @@ RSpec.describe Scimitar::Schema::Attribute do | |
| 46 46 | 
             
                  expect(attribute.errors.messages.to_h).to eql({userName: ['has the wrong type. It has to be a(n) string.']})
         | 
| 47 47 | 
             
                end
         | 
| 48 48 |  | 
| 49 | 
            +
                it 'is valid if multi-valued and type is string and given value is an array of strings' do
         | 
| 50 | 
            +
                  attribute = described_class.new(name: 'scopes', multiValued: true, type: 'string')
         | 
| 51 | 
            +
                  expect(attribute.valid?(['something', 'something else'])).to be(true)
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                it 'is valid if multi-valued and type is string and given value is an empty array' do
         | 
| 55 | 
            +
                  attribute = described_class.new(name: 'scopes', multiValued: true, type: 'string')
         | 
| 56 | 
            +
                  expect(attribute.valid?([])).to be(true)
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                it 'is invalid if multi-valued and type is string and given value is not an array' do
         | 
| 60 | 
            +
                  attribute = described_class.new(name: 'scopes', multiValued: true, type: 'string')
         | 
| 61 | 
            +
                  expect(attribute.valid?('something')).to be(false)
         | 
| 62 | 
            +
                  expect(attribute.errors.messages.to_h).to eql({scopes: ['or one of its elements has the wrong type. It has to be an array of strings.']})
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                it 'is invalid if multi-valued and type is string and given value is an array containing another type' do
         | 
| 66 | 
            +
                  attribute = described_class.new(name: 'scopes', multiValued: true, type: 'string')
         | 
| 67 | 
            +
                  expect(attribute.valid?(['something', 123])).to be(false)
         | 
| 68 | 
            +
                  expect(attribute.errors.messages.to_h).to eql({scopes: ['or one of its elements has the wrong type. It has to be an array of strings.']})
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 49 71 | 
             
                it 'is valid if type is boolean and given value is boolean' do
         | 
| 50 72 | 
             
                  expect(described_class.new(name: 'name', type: 'boolean').valid?(false)).to be(true)
         | 
| 51 73 | 
             
                  expect(described_class.new(name: 'name', type: 'boolean').valid?(true)).to be(true)
         |