scimitar 2.14.0 → 2.15.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5f63ca5b1572be0cb53a95a47280099713fd32be8f5650396a0db6a0c92550e
4
- data.tar.gz: 2f1f2a7ec7a6877607875f6b86e7b9859ae9124f71d295541847940dd31c8b1f
3
+ metadata.gz: 7c705a0369ccfd6204da2dd2a0e5a17a15f2fa658703218404d7556c1568a891
4
+ data.tar.gz: cd8ab1a8fc432533a6a0e4ee6045f20677c9164dfd330c416a68b5728b58947f
5
5
  SHA512:
6
- metadata.gz: '08626642f4b3e9dd29a6141c72bea784ab094ec197d9f3b9278957a59bfe230f4ef7ec84d52b1584127af647809351607bc01ef408d7269cc39dcf1f17180d49'
7
- data.tar.gz: 53845ba684f54f8e2e90e696b769aadfe803809fa3004fa35f779794d6e04bfa034ee231847382591cdc5600cc7544bbad4c959e2031c0271e67e5632d431f6c
6
+ metadata.gz: 11846119f8df609b6f75f95a436a78e2ca9dceed1a9d236cd08c89df052e45677efcee573943feaee719b7fff6f648bd13622a47d32f493bb69d3b8a58415363
7
+ data.tar.gz: 32bdc2ba6b4613c498ed988bfb2a4397c26f1d0196341f071b52d4bdb13065b9a658654dba1666bbb75c6a185afa924d6b6aa60d2f4ef9784cc9af6df4395623
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 RIPA Global
3
+ Copyright (c) 2026 RIPA Global c/o Andrew Hodgkinson
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/Rakefile CHANGED
@@ -1,7 +1,6 @@
1
1
  require 'rake'
2
2
  require 'rspec/core/rake_task'
3
3
  require 'rdoc/task'
4
- require 'sdoc'
5
4
 
6
5
  RSpec::Core::RakeTask.new(:default) do | t |
7
6
  end
@@ -12,5 +11,5 @@ Rake::RDocTask.new do | rd |
12
11
  rd.title = 'Scimitar'
13
12
  rd.main = 'README.md'
14
13
  rd.rdoc_dir = 'docs/rdoc'
15
- rd.generator = 'sdoc'
14
+ rd.generator = 'rdoc'
16
15
  end
@@ -36,11 +36,17 @@ module Scimitar
36
36
 
37
37
  pagination_info = scim_pagination_info(query.count())
38
38
 
39
- page_of_results = query
40
- .order(@id_column => :asc)
41
- .offset(pagination_info.offset)
42
- .limit(pagination_info.limit)
43
- .to_a()
39
+ # SCIM 2.0 RFC 7644: When count=0, return metadata only (no Resources).
40
+ # This avoids an unnecessary database query for record data.
41
+ page_of_results = if pagination_info.limit == 0
42
+ []
43
+ else
44
+ query
45
+ .order(@id_column => :asc)
46
+ .offset(pagination_info.offset)
47
+ .limit(pagination_info.limit)
48
+ .to_a()
49
+ end
44
50
 
45
51
  super(pagination_info, page_of_results) do | record |
46
52
  record_to_scim(record)
@@ -48,7 +48,7 @@ module Scimitar
48
48
  #
49
49
  def handle_scim_error(error_response, exception = error_response)
50
50
  unless Scimitar.engine_configuration.exception_reporter.nil?
51
- Scimitar.engine_configuration.exception_reporter.call(exception)
51
+ instance_exec(exception, &Scimitar.engine_configuration.exception_reporter)
52
52
  end
53
53
 
54
54
  render json: error_response, status: error_response.status
@@ -16,6 +16,7 @@ module Scimitar
16
16
  :application_controller_mixin,
17
17
  :exception_reporter,
18
18
  :optional_value_fields_required,
19
+ :render_mapped_nil_values_in_response,
19
20
  :schema_list_from_attribute_mappings,
20
21
  )
21
22
 
@@ -25,8 +26,9 @@ module Scimitar
25
26
  # Set defaults that may be overridden by the initializer.
26
27
  #
27
28
  defaults = {
28
- optional_value_fields_required: true,
29
- schema_list_from_attribute_mappings: []
29
+ optional_value_fields_required: true,
30
+ render_mapped_nil_values_in_response: true,
31
+ schema_list_from_attribute_mappings: []
30
32
  }
31
33
 
32
34
  super(defaults.merge(attributes))
@@ -15,9 +15,12 @@ module Scimitar
15
15
 
16
16
  # Set a limit (page size) value.
17
17
  #
18
- # +value+:: Integer value held in a String. Must be >= 1.
18
+ # +value+:: Integer value held in a String. Must be >= 0.
19
+ #
20
+ # Per SCIM 2.0 RFC 7644 Section 3.4.2.4: "A value of '0' indicates that
21
+ # no resource results are to be returned except for 'totalResults'."
19
22
  #
20
- # Raises exceptions if given non-numeric, zero or negative input.
23
+ # Raises exceptions if given non-numeric or negative input.
21
24
  #
22
25
  def limit=(value)
23
26
  value = value&.to_s
@@ -25,7 +28,7 @@ module Scimitar
25
28
 
26
29
  validate_numericality(value)
27
30
  input = value.to_i
28
- raise if input < 1
31
+ raise if input < 0
29
32
  @limit = input
30
33
  end
31
34
 
@@ -612,7 +612,13 @@ module Scimitar
612
612
  end
613
613
  end
614
614
 
615
- result.compact! if include_attributes.any?
615
+ if (
616
+ include_attributes.any? or
617
+ ! Scimitar.engine_configuration.render_mapped_nil_values_in_response
618
+ )
619
+ result.compact!
620
+ end
621
+
616
622
  result
617
623
 
618
624
  when Array # Static or dynamic mapping against lists in data source
@@ -102,7 +102,9 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
102
102
  # JSON response to the API caller. If you want exceptions to also be
103
103
  # reported to a third party system such as sentry.io or raygun.com, you can
104
104
  # configure a Proc to do so. It is passed a Ruby exception subclass object.
105
- # For example, a minimal sentry.io reporter might do this:
105
+ # The Proc is called via 'instance_exec' in the controller context, so you
106
+ # have access to things like 'request', 'params' and 'action_name'. For
107
+ # example, a minimal sentry.io reporter might do this:
106
108
  #
107
109
  # exception_reporter: Proc.new do | exception |
108
110
  # Sentry.capture_exception(exception)
@@ -119,6 +121,12 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
119
121
  #
120
122
  # optional_value_fields_required: false
121
123
 
124
+ # When rendering responses, +nil+ values can either still be included via
125
+ # the attributes map with a JSON value of +null+, or omitted. By default,
126
+ # all attributes in your map are returned in responses.
127
+ #
128
+ # render_mapped_nil_values_in_response: false
129
+
122
130
  # The SCIM standard `/Schemas` endpoint lists, by default, all known schema
123
131
  # definitions with the mutabilty (read-write, read-only, write-only) state
124
132
  # described by those definitions, and includes all defined attributes. For
@@ -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.14.0'
6
+ VERSION = '2.15.0'
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 = '2025-11-14'
11
+ DATE = '2026-03-06'
12
12
 
13
13
  end
@@ -352,8 +352,9 @@ RSpec.describe Scimitar::ApplicationController do
352
352
  context 'with an exception reporter' do
353
353
  around :each do | example |
354
354
  original_configuration = Scimitar.engine_configuration.exception_reporter
355
+ exceptions = @exceptions = []
355
356
  Scimitar.engine_configuration.exception_reporter = Proc.new do | exception |
356
- @exception = exception
357
+ exceptions << exception
357
358
  end
358
359
  example.run()
359
360
  ensure
@@ -364,8 +365,8 @@ RSpec.describe Scimitar::ApplicationController do
364
365
  it 'is invoked' do
365
366
  get :index, params: { format: :scim }
366
367
 
367
- expect(@exception).to be_a(RuntimeError)
368
- expect(@exception.message).to eql('Bang')
368
+ expect(@exceptions.first).to be_a(RuntimeError)
369
+ expect(@exceptions.first.message).to eql('Bang')
369
370
  end
370
371
  end
371
372
 
@@ -379,8 +380,8 @@ RSpec.describe Scimitar::ApplicationController do
379
380
  it 'is invoked' do
380
381
  get :index, params: { format: :scim }
381
382
 
382
- expect(@exception).to be_a(ActiveRecord::RecordNotFound)
383
- expect(@exception.message).to eql('42')
383
+ expect(@exceptions.first).to be_a(ActiveRecord::RecordNotFound)
384
+ expect(@exceptions.first.message).to eql('42')
384
385
  end
385
386
  end
386
387
 
@@ -398,8 +399,8 @@ RSpec.describe Scimitar::ApplicationController do
398
399
  it 'is invoked' do
399
400
  get :index, params: { format: :scim }
400
401
 
401
- expect(@exception).to be_a(ActionDispatch::Http::Parameters::ParseError)
402
- expect(@exception.message).to eql('Hello')
402
+ expect(@exceptions.first).to be_a(ActionDispatch::Http::Parameters::ParseError)
403
+ expect(@exceptions.first.message).to eql('Hello')
403
404
  end
404
405
  end
405
406
 
@@ -412,8 +413,8 @@ RSpec.describe Scimitar::ApplicationController do
412
413
  request.headers['Content-Type'] = 'text/plain'
413
414
  get :index
414
415
 
415
- expect(@exception).to be_a(Scimitar::ErrorResponse)
416
- expect(@exception.message).to eql('Only application/scim+json type is accepted.')
416
+ expect(@exceptions.first).to be_a(Scimitar::ErrorResponse)
417
+ expect(@exceptions.first.message).to eql('Only application/scim+json type is accepted.')
417
418
  end
418
419
  end
419
420
 
@@ -423,16 +424,16 @@ RSpec.describe Scimitar::ApplicationController do
423
424
  request.headers['User-Agent' ] = 'Google-Auto-Provisioning'
424
425
  get :index
425
426
 
426
- expect(@exception).to be_a(RuntimeError)
427
- expect(@exception.message).to eql('Bang')
427
+ expect(@exceptions.first).to be_a(RuntimeError)
428
+ expect(@exceptions.first.message).to eql('Bang')
428
429
  end
429
430
 
430
431
  it 'is invoked early for unrecognised agents' do
431
432
  request.headers['Content-Type'] = 'application/json'
432
433
  get :index
433
434
 
434
- expect(@exception).to be_a(Scimitar::ErrorResponse)
435
- expect(@exception.message).to eql('Only application/scim+json type is accepted.')
435
+ expect(@exceptions.first).to be_a(Scimitar::ErrorResponse)
436
+ expect(@exceptions.first.message).to eql('Only application/scim+json type is accepted.')
436
437
  end
437
438
  end # "context 'and with Google SCIM calls' do"
438
439
 
@@ -459,8 +460,8 @@ RSpec.describe Scimitar::ApplicationController do
459
460
  request.headers['Content-Type'] = 'application/json+success'
460
461
  get :index
461
462
 
462
- expect(@exception).to be_a(RuntimeError)
463
- expect(@exception.message).to eql('Bang')
463
+ expect(@exceptions.first).to be_a(RuntimeError)
464
+ expect(@exceptions.first.message).to eql('Bang')
464
465
 
465
466
  expect(request.format == :scim).to eql(true)
466
467
  expect(request.headers['CONTENT_TYPE']).to eql('application/scim+json')
@@ -472,8 +473,8 @@ RSpec.describe Scimitar::ApplicationController do
472
473
  request.headers['Content-Type'] = 'application/json+preserve'
473
474
  get :index
474
475
 
475
- expect(@exception).to be_a(RuntimeError)
476
- expect(@exception.message).to eql('Bang')
476
+ expect(@exceptions.first).to be_a(RuntimeError)
477
+ expect(@exceptions.first.message).to eql('Bang')
477
478
 
478
479
  expect(request.format == :html).to eql(true)
479
480
  expect(request.headers['CONTENT_TYPE']).to eql('application/json+preserve')
@@ -485,11 +486,28 @@ RSpec.describe Scimitar::ApplicationController do
485
486
  request.headers['Content-Type'] = 'application/json+fail'
486
487
  get :index
487
488
 
488
- expect(@exception).to be_a(Scimitar::ErrorResponse)
489
- expect(@exception.message).to eql('Only application/scim+json type is accepted.')
489
+ expect(@exceptions.first).to be_a(Scimitar::ErrorResponse)
490
+ expect(@exceptions.first.message).to eql('Only application/scim+json type is accepted.')
490
491
  end
491
492
  end # "context 'returning "fail"' do"
492
493
  end # "context 'and with a custom request sanitizer' do"
494
+
495
+ context 'evaluated in controller context' do
496
+ it 'has access to controller methods like request and params' do
497
+ reported_request_method = nil
498
+ reported_params = nil
499
+ Scimitar.engine_configuration.exception_reporter = Proc.new do | exception |
500
+ reported_request_method = request.method
501
+ reported_params = params
502
+ end
503
+
504
+ get :index, params: { format: :scim, foo: 'bar' }
505
+
506
+ expect(reported_request_method).to eql('GET')
507
+ expect(reported_params['format']).to eql('scim')
508
+ expect(reported_params['foo']).to eql('bar')
509
+ end
510
+ end
493
511
  end # "context 'exception reporter' do"
494
512
  end # "context 'error handling' do"
495
513
  end
@@ -34,12 +34,17 @@ RSpec.describe Scimitar::Lists::Count do
34
34
  expect { @instance.limit = 'A' }.to raise_error(RuntimeError)
35
35
  end
36
36
 
37
- it 'complains about attempts to set zero values' do
38
- expect { @instance.limit = '0' }.to raise_error(RuntimeError)
37
+ it 'allows count=0 per SCIM 2.0 specification (RFC 7644)' do
38
+ expect { @instance.limit = '0' }.to_not raise_error
39
+ expect(@instance.limit).to eql(0)
39
40
  end
40
41
 
41
- it 'complains about attempts to set zero values' do
42
+ it 'allows count=0 as integer' do
43
+ expect { @instance.limit = 0 }.to_not raise_error
44
+ expect(@instance.limit).to eql(0)
45
+ end
42
46
 
47
+ it 'complains about attempts to set negative values' do
43
48
  expect { @instance.limit = '-10' }.to raise_error(RuntimeError)
44
49
  end
45
50
  end # "context 'on-read error checking' do"
@@ -267,7 +267,7 @@ RSpec.describe Scimitar::Resources::Mixin do
267
267
  instance.first_name = 'Foo'
268
268
  instance.last_name = 'Bar'
269
269
  instance.work_email_address = 'foo.bar@test.com'
270
- instance.home_email_address = nil
270
+ instance.home_email_address = 'foo.bar@example.com'
271
271
  instance.work_phone_number = '+642201234567'
272
272
  instance.organization = 'SOMEORG'
273
273
 
@@ -299,6 +299,49 @@ RSpec.describe Scimitar::Resources::Mixin do
299
299
  'urn:ietf:params:scim:schemas:extension:manager:1.0:User' => {},
300
300
  })
301
301
  end
302
+
303
+ it 'hides "nil" value attributes' do
304
+ uuid = SecureRandom.uuid
305
+
306
+ instance = MockUser.new
307
+ instance.primary_key = uuid
308
+ instance.scim_uid = 'AA02984'
309
+ instance.username = nil
310
+ instance.password = 'correcthorsebatterystaple'
311
+ instance.first_name = nil
312
+ instance.last_name = 'Bar'
313
+ instance.work_email_address = 'foo.bar@test.com'
314
+ instance.home_email_address = 'foo.bar@example.com'
315
+ instance.work_phone_number = '+642201234567'
316
+ instance.organization = 'SOMEORG'
317
+
318
+ g1 = MockGroup.create!(display_name: 'Group 1')
319
+ g2 = MockGroup.create!(display_name: 'Group 2')
320
+ g3 = MockGroup.create!(display_name: 'Group 3')
321
+
322
+ g1.mock_users << instance
323
+ g3.mock_users << instance
324
+
325
+ scim = instance.to_scim(location: "https://test.com/mock_users/#{uuid}", include_attributes: %w[id userName name groups.display groups.value organization])
326
+ json = scim.to_json()
327
+ hash = JSON.parse(json)
328
+
329
+ expect(hash).to eql({
330
+ 'id' => uuid,
331
+ 'name' => {'familyName'=>'Bar'},
332
+ 'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
333
+ 'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
334
+ 'schemas' => [
335
+ 'urn:ietf:params:scim:schemas:core:2.0:User',
336
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User',
337
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User',
338
+ ],
339
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
340
+ 'organization' => 'SOMEORG',
341
+ },
342
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User' => {},
343
+ })
344
+ end
302
345
  end # "context 'with list of requested attributes' do"
303
346
 
304
347
  context 'with a UUID, renamed primary key column' do
@@ -332,7 +375,7 @@ RSpec.describe Scimitar::Resources::Mixin do
332
375
  'userName' => 'foo',
333
376
  'name' => {'givenName'=>'Foo', 'familyName'=>'Bar'},
334
377
  'active' => true,
335
- 'emails' => [{'type'=>'work', 'primary'=>true, 'value'=>'foo.bar@test.com'}, {"primary"=>false, "type"=>"home", "value"=>nil}],
378
+ 'emails' => [{'type'=>'work', 'primary'=>true, 'value'=>'foo.bar@test.com'}, {'primary'=>false, 'type'=>'home', 'value'=>nil}],
336
379
  'phoneNumbers'=> [{'type'=>'work', 'primary'=>false, 'value'=>'+642201234567'}],
337
380
  'id' => uuid,
338
381
  'externalId' => 'AA02984',
@@ -353,6 +396,71 @@ RSpec.describe Scimitar::Resources::Mixin do
353
396
  },
354
397
  })
355
398
  end
399
+
400
+ context 'and when configured to omit "nil" values in the response' do
401
+ around :each do | example |
402
+ original_configuration = Scimitar.engine_configuration.render_mapped_nil_values_in_response
403
+ Scimitar.engine_configuration.render_mapped_nil_values_in_response = false
404
+ example.run()
405
+ ensure
406
+ Scimitar.engine_configuration.render_mapped_nil_values_in_response = original_configuration
407
+ end
408
+
409
+ it 'omits "nil" values as expected' do
410
+ uuid = SecureRandom.uuid
411
+
412
+ instance = MockUser.new
413
+ instance.primary_key = uuid
414
+ instance.scim_uid = 'AA02984'
415
+ instance.username = 'foo'
416
+ instance.password = 'correcthorsebatterystaple'
417
+ instance.first_name = nil
418
+ instance.last_name = 'Bar'
419
+ instance.work_email_address = 'foo.bar@test.com'
420
+ instance.home_email_address = nil
421
+ instance.work_phone_number = '+642201234567'
422
+ instance.organization = 'SOMEORG'
423
+
424
+ g1 = MockGroup.create!(display_name: 'Group 1')
425
+ g2 = MockGroup.create!(display_name: 'Group 2')
426
+ g3 = MockGroup.create!(display_name: 'Group 3')
427
+
428
+ g1.mock_users << instance
429
+ g3.mock_users << instance
430
+
431
+ scim = instance.to_scim(location: "https://test.com/mock_users/#{uuid}")
432
+ json = scim.to_json()
433
+ hash = JSON.parse(json)
434
+
435
+ # Note currently limited implementation for things like static maps,
436
+ # where part of the returned value is included; in this case, the
437
+ # "primary" value is "false" for e-mail of type "home", so the
438
+ # structure for that *does* appear in the output array even though
439
+ # the source dynamic data field from the NockUser instance is "nil".
440
+ #
441
+ expect(hash).to eql({
442
+ 'userName' => 'foo',
443
+ 'name' => {'familyName'=>'Bar'},
444
+ 'active' => true,
445
+ 'emails' => [{'type'=>'work', 'primary'=>true, 'value'=>'foo.bar@test.com'}, {'primary' => false, 'type' => 'home'}],
446
+ 'phoneNumbers'=> [{'type'=>'work', 'primary'=>false, 'value'=>'+642201234567'}],
447
+ 'id' => uuid,
448
+ 'externalId' => 'AA02984',
449
+ 'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
450
+ 'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
451
+ 'schemas' => [
452
+ 'urn:ietf:params:scim:schemas:core:2.0:User',
453
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User',
454
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User',
455
+ ],
456
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
457
+ 'organization' => 'SOMEORG',
458
+ 'primaryEmail' => instance.work_email_address,
459
+ },
460
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User' => {}
461
+ })
462
+ end
463
+ end # "context 'and when configured to omit "nil" values in the response'" do"
356
464
  end # "context 'with a UUID, renamed primary key column' do"
357
465
 
358
466
  context 'with an integer, conventionally named primary key column' do
@@ -292,6 +292,92 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
292
292
  usernames = result['Resources'].map { |resource| resource['userName'] }
293
293
  expect(usernames).to match_array(['2', '3'])
294
294
  end
295
+
296
+ # SCIM 2.0 RFC 7644 Section 3.4.2.4: count=0 support
297
+ context 'with count=0' do
298
+ it 'returns 200 OK' do
299
+ get '/Users', params: {
300
+ format: :scim,
301
+ count: 0
302
+ }
303
+
304
+ expect(response.status).to eql(200)
305
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
306
+ end
307
+
308
+ it 'returns totalResults with actual count' do
309
+ get '/Users', params: {
310
+ format: :scim,
311
+ count: 0
312
+ }
313
+
314
+ result = JSON.parse(response.body)
315
+ expect(result['totalResults']).to eql(3)
316
+ end
317
+
318
+ it 'returns itemsPerPage as 0' do
319
+ get '/Users', params: {
320
+ format: :scim,
321
+ count: 0
322
+ }
323
+
324
+ result = JSON.parse(response.body)
325
+ expect(result['itemsPerPage']).to eql(0)
326
+ end
327
+
328
+ it 'returns empty Resources array' do
329
+ get '/Users', params: {
330
+ format: :scim,
331
+ count: 0
332
+ }
333
+
334
+ result = JSON.parse(response.body)
335
+ expect(result['Resources']).to eql([])
336
+ end
337
+
338
+ it 'respects startIndex parameter' do
339
+ get '/Users', params: {
340
+ format: :scim,
341
+ count: 0,
342
+ startIndex: 5
343
+ }
344
+
345
+ result = JSON.parse(response.body)
346
+ expect(result['startIndex']).to eql(5)
347
+ end
348
+
349
+ it 'applies filters when calculating totalResults' do
350
+ get '/Users', params: {
351
+ format: :scim,
352
+ count: 0,
353
+ filter: 'name.familyName eq "Bar"'
354
+ }
355
+
356
+ result = JSON.parse(response.body)
357
+ expect(result['totalResults']).to eql(1)
358
+ expect(result['Resources']).to eql([])
359
+ end
360
+
361
+ it 'does not query for records (performance optimization)' do
362
+ # We should get the count but not fetch records
363
+ query_double = instance_double(ActiveRecord::Relation)
364
+ allow(MockUser).to receive(:all).and_return(query_double)
365
+ allow(query_double).to receive(:count).and_return(3)
366
+
367
+ # Should NOT call order, offset, limit, or to_a when count=0
368
+ expect(query_double).not_to receive(:order)
369
+ expect(query_double).not_to receive(:offset)
370
+ expect(query_double).not_to receive(:limit)
371
+ expect(query_double).not_to receive(:to_a)
372
+
373
+ get '/Users', params: {
374
+ format: :scim,
375
+ count: 0
376
+ }
377
+
378
+ expect(response.status).to eql(200)
379
+ end
380
+ end # "context 'with count=0' do"
295
381
  end # "context 'with items' do"
296
382
 
297
383
  context 'with bad calls' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scimitar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.14.0
4
+ version: 2.15.0
5
5
  platform: ruby
6
6
  authors:
7
- - RIPA Global
8
7
  - Andrew David Hodgkinson
8
+ - RIPA Global
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-14 00:00:00.000000000 Z
11
+ date: 2026-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '6.15'
89
+ version: '7.2'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '6.15'
96
+ version: '7.2'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: warden
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -296,7 +296,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
296
296
  - !ruby/object:Gem::Version
297
297
  version: '0'
298
298
  requirements: []
299
- rubygems_version: 3.6.2
299
+ rubygems_version: 4.0.3
300
300
  specification_version: 4
301
301
  summary: SCIM v2 for Rails
302
302
  test_files: