scimitar 2.5.0 → 2.6.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/resources/base.rb +12 -2
- data/app/models/scimitar/schema/attribute.rb +14 -5
- data/app/models/scimitar/schema/base.rb +1 -1
- 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 +2 -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 +2 -1
- 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/resources/base_spec.rb +38 -4
- data/spec/models/scimitar/resources/mixin_spec.rb +4 -1
- data/spec/models/scimitar/schema/attribute_spec.rb +22 -0
- data/spec/requests/active_record_backed_resources_controller_spec.rb +125 -33
- 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
|
|
@@ -93,14 +93,23 @@ module Scimitar
|
|
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
|
@@ -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 = '2.
|
6
|
+
VERSION = '2.6.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
|
@@ -10,6 +10,7 @@ class MockUser < ActiveRecord::Base
|
|
10
10
|
primary_key
|
11
11
|
scim_uid
|
12
12
|
username
|
13
|
+
password
|
13
14
|
first_name
|
14
15
|
last_name
|
15
16
|
work_email_address
|
@@ -46,6 +47,7 @@ class MockUser < ActiveRecord::Base
|
|
46
47
|
id: :primary_key,
|
47
48
|
externalId: :scim_uid,
|
48
49
|
userName: :username,
|
50
|
+
password: :password,
|
49
51
|
name: {
|
50
52
|
givenName: :first_name,
|
51
53
|
familyName: :last_name
|
@@ -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'
|
@@ -10,7 +10,7 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
12
12
|
|
13
|
-
ActiveRecord::Schema[7.
|
13
|
+
ActiveRecord::Schema[7.1].define(version: 2021_03_08_044214) do
|
14
14
|
# These are extensions that must be enabled in order to support this database
|
15
15
|
enable_extension "plpgsql"
|
16
16
|
|
@@ -33,6 +33,7 @@ ActiveRecord::Schema[7.0].define(version: 2021_03_08_044214) do
|
|
33
33
|
t.datetime "updated_at", null: false
|
34
34
|
t.text "scim_uid"
|
35
35
|
t.text "username"
|
36
|
+
t.text "password"
|
36
37
|
t.text "first_name"
|
37
38
|
t.text "last_name"
|
38
39
|
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
|
@@ -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')
|
@@ -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)
|